feat: Add timing and QUIET mode to Makefile#121
Conversation
0f34a55 to
21a35da
Compare
9cbe6d3 to
a206e83
Compare
|
Yikes, so many conflicts. |
c94ded3 to
c3550a8
Compare
c3550a8 to
35b8022
Compare
There was a problem hiding this comment.
Pull request overview
Adds build-phase timing instrumentation and a QUIET mode, plus optional CPU/memory metrics collection and plotting, and wires quiet/timing/metrics into CI.
Changes:
- Introduces timing collection and a
timing-summarytarget for phase duration/status reporting. - Adds
QUIET=1and metrics collection/plotting helpers (bash + Python) for CI observability. - Updates CI workflow to run builds/tests in quiet mode and upload logs/timing/metrics artifacts.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| mk/timing-summary.sh | Generates a timing table and writes _build/timing/summary.txt from phase timestamp/status files. |
| mk/run-phase.sh | Adds a wrapper to time phases and capture logs in quiet mode. |
| mk/collect-metrics.sh | Background CPU/memory sampler writing _build/metrics/metrics.csv. |
| mk/plot-metrics.py | Produces SVG charts overlaying metrics with phase timing regions. |
| Makefile | Adds QUIET/metrics/timing variables, timing macros, and make targets for summaries/metrics. |
| .github/workflows/ci.yml | Enables QUIET mode in CI, collects metrics, uploads artifacts, and writes a summary. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ifeq ($(QUIET),1) | ||
| LOG = @: | ||
| LOG_GROUP_START = @: | ||
| LOG_GROUP_END = @: | ||
| endif |
There was a problem hiding this comment.
QUIET=1 currently only disables the LOG* helper macros, but it won’t suppress normal recipe command echoing (most recipe lines are not @-prefixed). This doesn’t meet the stated goal of 'suppress output unless there’s an error'. Consider enabling Make’s silent mode when QUIET=1 (e.g., set MAKEFLAGS += --silent / .SILENT: under the conditional), and then explicitly echo/print only for error reporting.
| $(STAGE2_CABAL_BUILD) rts | ||
| $(call PHASE_END_OK,stage2.rts) | ||
|
|
||
| $(call PHASE_START,stage2.executables) | ||
| $(call LOG,Building executables $(STAGE2_EXECUTABLES)) | ||
| $(STAGE2_CABAL_BUILD) $(addprefix exe:,$(STAGE2_EXECUTABLES)) | ||
| $(call PHASE_END_OK,stage2.executables) | ||
|
|
||
| $(call PHASE_START,stage2.libraries) | ||
| $(call LOG,Building libraries $(filter-out rts%,$(STAGE2_LIBRARIES))) | ||
| $(STAGE2_CABAL_BUILD) $(filter-out rts%,$(STAGE2_LIBRARIES)) | ||
| $(call PHASE_END_OK,stage2.libraries) |
There was a problem hiding this comment.
Phase timing/status files are only finalized on success (PHASE_END_OK). If the build command fails, .end and .status won’t be recorded for that phase, and mk/timing-summary.sh will omit the phase entirely (it discovers only phases with both .start and .end). To support 'timing summary at end (even on failure)', wrap phase commands so failures also write .end and a failure status (e.g., a PHASE_END_FAIL macro, or a common wrapper that records end+status in both branches).
| $(STAGE2_CABAL_BUILD) rts | |
| $(call PHASE_END_OK,stage2.rts) | |
| $(call PHASE_START,stage2.executables) | |
| $(call LOG,Building executables $(STAGE2_EXECUTABLES)) | |
| $(STAGE2_CABAL_BUILD) $(addprefix exe:,$(STAGE2_EXECUTABLES)) | |
| $(call PHASE_END_OK,stage2.executables) | |
| $(call PHASE_START,stage2.libraries) | |
| $(call LOG,Building libraries $(filter-out rts%,$(STAGE2_LIBRARIES))) | |
| $(STAGE2_CABAL_BUILD) $(filter-out rts%,$(STAGE2_LIBRARIES)) | |
| $(call PHASE_END_OK,stage2.libraries) | |
| set +e; \ | |
| $(STAGE2_CABAL_BUILD) rts; status=$$?; \ | |
| if [ $$status -eq 0 ]; then \ | |
| $(call PHASE_END_OK,stage2.rts); \ | |
| else \ | |
| $(call PHASE_END_FAIL,stage2.rts); \ | |
| fi; \ | |
| exit $$status | |
| $(call PHASE_START,stage2.executables) | |
| $(call LOG,Building executables $(STAGE2_EXECUTABLES)) | |
| set +e; \ | |
| $(STAGE2_CABAL_BUILD) $(addprefix exe:,$(STAGE2_EXECUTABLES)); status=$$?; \ | |
| if [ $$status -eq 0 ]; then \ | |
| $(call PHASE_END_OK,stage2.executables); \ | |
| else \ | |
| $(call PHASE_END_FAIL,stage2.executables); \ | |
| fi; \ | |
| exit $$status | |
| $(call PHASE_START,stage2.libraries) | |
| $(call LOG,Building libraries $(filter-out rts%,$(STAGE2_LIBRARIES))) | |
| set +e; \ | |
| $(STAGE2_CABAL_BUILD) $(filter-out rts%,$(STAGE2_LIBRARIES)); status=$$?; \ | |
| if [ $$status -eq 0 ]; then \ | |
| $(call PHASE_END_OK,stage2.libraries); \ | |
| else \ | |
| $(call PHASE_END_FAIL,stage2.libraries); \ | |
| fi; \ | |
| exit $$status |
| - name: Display build timings | ||
| if: ${{ !cancelled() }} | ||
| shell: devx {0} | ||
| run: make timing-summary |
There was a problem hiding this comment.
In GitHub Actions, steps after a failing step are skipped unless they use if: always() .... With the current condition, the 'Display build timings' step will not run when the build fails, contradicting the PR goal of showing timing summaries even on failure. Use an always() guard (optionally combined with !cancelled()) for timing display and also for stopping metrics collection / uploading diagnostics that should run after failures.
| # Record start time | ||
| START_TIME=$(date +%s) | ||
| echo "$START_TIME" > "$TIMING_DIR/$PHASE.start" | ||
| echo ">>> Building $PHASE..." |
There was a problem hiding this comment.
The script’s contract says quiet mode should 'suppress output unless error', but it always prints >>> Building ... regardless of QUIET. Gate this message on QUIET!=1 (or send it to the log in quiet mode) so quiet mode is actually quiet unless there’s a failure.
| echo ">>> Building $PHASE..." | |
| if [[ "$QUIET" != "1" ]]; then | |
| echo ">>> Building $PHASE..." | |
| fi |
| local parent=$1 | ||
| for sp in "${sub_phases[@]}"; do | ||
| # Match: parent.anything (no further dots) | ||
| if [[ "$sp" == "$parent".* ]]; then |
There was a problem hiding this comment.
The comment says sub-phases match parent.anything (no further dots), but the glob \"$parent\".* will also include nested names like parent.a.b. Either adjust the match to enforce a single-dot sub-phase (e.g., exclude \"$parent\".*.*) or update the comment to reflect the actual behavior.
| if [[ "$sp" == "$parent".* ]]; then | |
| if [[ "$sp" == "$parent".* && "$sp" != "$parent".*.* ]]; then |
| elif phase_name.startswith('stage3-'): | ||
| # Assign colors from palette based on platform name hash | ||
| idx = hash(phase_name) % len(stage3_palette) | ||
| color = stage3_palette[idx] |
There was a problem hiding this comment.
Using Python’s built-in hash() makes stage3 color assignment non-deterministic across runs (hash randomization), which makes comparing plots over time harder. Use a stable hash (e.g., hashlib.sha256(phase_name.encode()).digest() to derive idx) so the same platform always gets the same color.
15ef2db to
e41e2a4
Compare
Build instrumentation: - QUIET=1 suppresses LOG/LOG_GROUP output for cleaner CI logs - PHASE_START/PHASE_END_OK/PHASE_END_FAIL macros record timestamps - Sub-phase granularity for stage1, stage2, and stage3 phases - mk/run-phase.sh: standalone wrapper combining timing + quiet mode - mk/timing-summary.sh: auto-discovers phases, displays ASCII table with indented sub-phases and TOTAL row - .DEFAULT_GOAL := all to prevent timing-summary as default target CI metrics collection and visualization: - mk/collect-metrics.sh: background CPU/memory sampler (cross-platform) - mk/plot-metrics.py: generates dual-axis SVG plots with phase overlays - CI workflow: collect metrics, generate plots, upload to R2 - CI workflow: QUIET=1 for cleaner logs, timing tables in step summary
e41e2a4 to
467f894
Compare
Two issues from the QUIET mode CI run (#121): 1. **Timing overwrites:** The `test: stage2` dependency triggers the PHONY stage2 recipe again. Since everything is already built, the re-run completes in seconds, overwriting the real stage2 timings (16m 31s) with near-zero durations (3m 36s). Fix: guard PHASE_START/PHASE_END macros to skip writing if the phase already completed successfully (status=0). Failed phases can still be retried. 2. **Metrics plots:** The "Generate Metrics plots" CI step used `nix-shell -p ...` which is not available in the devx environment. Fix: replace `nix-shell` with `nix build --print-out-paths` to get the store path, then invoke the binary directly. Applied to both the matplotlib plot generation and the awscli R2 upload steps.
Two issues from the QUIET mode CI run (#121): 1. **Timing overwrites:** The `test: stage2` dependency triggers the PHONY stage2 recipe again. Since everything is already built, the re-run completes in seconds, overwriting the real stage2 timings (16m 31s) with near-zero durations (3m 36s). Fix: guard PHASE_START/PHASE_END macros to skip writing if the phase already completed successfully (status=0). Failed phases can still be retried. 2. **Metrics plots:** The "Generate Metrics plots" CI step used `nix-shell -p ...` which is not available in the devx environment. Fix: replace `nix-shell` with `nix build --print-out-paths` to get the store path, then invoke the binary directly. Applied to both the matplotlib plot generation and the awscli R2 upload steps.
hasufell
left a comment
There was a problem hiding this comment.
Looks like a lot of LLM generated stuff.
| @@ -0,0 +1,221 @@ | |||
| #!/usr/bin/env bash | |||
There was a problem hiding this comment.
This isn't more portable than /bin/bash. /usr/bin/env isn't guaranteed by posix and we're now potentially executing a non-system bash.
I would suggest to write posix scripts, unless we really need bash features. It's also hard to know what bash version is on the users system, e.g. mac sometimes ships very old bash versions that lack features of modern bash.
| echo "0" | ||
| fi | ||
| ;; | ||
| *) |
| #!/usr/bin/env bash | ||
| # collect-metrics.sh - Collect CPU and memory metrics during build | ||
| # | ||
| # Usage: collect-metrics.sh start METRICS_DIR [INTERVAL] |
There was a problem hiding this comment.
| # Usage: collect-metrics.sh start METRICS_DIR [INTERVAL] | |
| # Usage: collect-metrics.sh start [METRICS_DIR] [INTERVAL] |
METRICS_DIR appears to be optional, since it has a default.
| OS="$(uname -s)" | ||
|
|
||
| # State file for CPU delta calculation | ||
| CPU_STATE_FILE="" |
There was a problem hiding this comment.
Why is this variable set in run_collector, but not here?
| # Calculates delta between samples for accurate instantaneous usage | ||
| get_cpu_usage() { | ||
| case "$OS" in | ||
| Darwin) |
There was a problem hiding this comment.
For long case expressions, it's better to factor the code out into get_cpu_usage_darwin() etc. and then have a simple case expression that disambiguates by platform.
| done | ||
|
|
||
| # Define display order for top-level phases | ||
| ordered_top_level() { |
There was a problem hiding this comment.
Please make all the used variables p, t etc. local.
| for t in "${top_level[@]}"; do | ||
| local found=0 | ||
| for o in "${ordered[@]}"; do | ||
| [[ "$t" == "$o" ]] && found=1 && break | ||
| done | ||
| [[ $found -eq 0 ]] && ordered+=("$t") |
There was a problem hiding this comment.
I've already written a function like this:
# @FUNCTION: array_contains
# @USAGE: <arr_ref> <val>
# @DESCRIPTION:
# Checks whether the array reference contains the given value.
# @RETURN: 0 if value exists, 1 otherwise
array_contains() {
local -n arr=$1
local val=$2
shift 2
if [[ " ${arr[*]} " =~ [[:space:]]${val}[[:space:]] ]]; then
return 0
else
return 1
fi
}You pass it the array reference.
|
|
||
| # Get sub-phases for a top-level phase, sorted alphabetically | ||
| sub_phases_of() { | ||
| local parent=$1 |
There was a problem hiding this comment.
Check function argument before use.
| sub_phases_of() { | ||
| local parent=$1 | ||
| for sp in "${sub_phases[@]}"; do | ||
| # Match: parent.suffix (single level only, no nested dots) |
| printf "| %-${COL_PHASE}s | %13s | |\n" "TOTAL" "$total_str" | ||
| echo "$sep" | ||
|
|
||
| # Save summary to file (top-level phases only, for machine consumption) |
There was a problem hiding this comment.
The code below also saves sub-phases, so the comment seems wrong.
collect-metrics.sh: - Factor case branches into named helpers (get_cpu_usage_darwin, get_cpu_usage_linux, get_memory_usage_darwin, get_memory_usage_linux) with thin dispatchers for the cross-platform functions - Initialize CPU_STATE_FILE at declaration instead of in run_collector() - Replace all `|| echo <default>` fallbacks with `exit 1` + error on stderr — silent zeros hide real failures in CI - Document /proc/stat guard (minimal containers without procfs) - Document SIGTERM choice in stop command - Fix usage message: METRICS_DIR is optional (brackets) - Add bash dependency note after shebang plot-metrics.py: - Add 5-stage data pipeline overview comment (COLLECT → READ → SELECT → FILTER → PLOT) - Add create_plot() docstring covering dual y-axes, phase shading, label alternation, and all arguments - Add inline comments explaining effective cores calculation, color assignment logic, and alternating label positions
collect-metrics.sh: - Factor case branches into named helpers (get_cpu_usage_darwin, get_cpu_usage_linux, get_memory_usage_darwin, get_memory_usage_linux) with thin dispatchers for the cross-platform functions - Initialize CPU_STATE_FILE at declaration instead of in run_collector() - Replace all `|| echo <default>` fallbacks with `exit 1` + error on stderr — silent zeros hide real failures in CI - Document /proc/stat guard (minimal containers without procfs) - Document SIGTERM choice in stop command - Fix usage message: METRICS_DIR is optional (brackets) - Add bash dependency note after shebang plot-metrics.py: - Add 5-stage data pipeline overview comment (COLLECT → READ → SELECT → FILTER → PLOT) - Add create_plot() docstring covering dual y-axes, phase shading, label alternation, and all arguments - Add inline comments explaining effective cores calculation, color assignment logic, and alternating label positions
collect-metrics.sh: - Factor case branches into named helpers (get_cpu_usage_darwin, get_cpu_usage_linux, get_memory_usage_darwin, get_memory_usage_linux) with thin dispatchers for the cross-platform functions - Initialize CPU_STATE_FILE at declaration instead of in run_collector() - Replace all `|| echo <default>` fallbacks with `exit 1` + error on stderr — silent zeros hide real failures in CI - Document /proc/stat guard (minimal containers without procfs) - Document SIGTERM choice in stop command - Fix usage message: METRICS_DIR is optional (brackets) - Add bash dependency note after shebang plot-metrics.py: - Add 5-stage data pipeline overview comment (COLLECT → READ → SELECT → FILTER → PLOT) - Add create_plot() docstring covering dual y-axes, phase shading, label alternation, and all arguments - Add inline comments explaining effective cores calculation, color assignment logic, and alternating label positions
Summary
QUIET=1mode to suppress output unless there's an error_build/timing/summary.txtQUIET=1in devx CI workflowUsage
Example Output
Test plan
makeand verify timing summary appears at endmake QUIET=1and verify output is suppressed_build/logs/_build/timing/summary.txt