project-chip / project-chip/certification-tool
Perf: end-of-test-case log replay + single end-of-run DB commit cause long tail latency on constrained hosts (e.g. Raspberry Pi)
@rquidute is already working on this.
Since Sep 17, 2026.
- Dominant language
- Shell
- Stars
- 83
- Forks
- 47
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 17
Description
Summary
When ENABLE_REALTIME_PYTHON_TEST_LOGS=False (the default), the last step of a python_testing test case ("Show test logs") replays the entire SDK stdout log file line-by-line with an artificial pacing delay, and the whole run's log/state is committed to the DB in a single operation at the very end of the run. On resource-constrained hosts (e.g. Raspberry Pi), with large log volumes this adds tens of seconds to minutes of wall-clock time even though actual test execution is fast. There's a matching set of smaller issues on the CLI side that compound the perceived lag when attached to a run with test-run-execution.
Root cause
backend/test_collections/matter/sdk_tests/support/python_testing/models/test_case.py:69-71:
LOG_BATCH_SIZE = 50 # Number of log lines to send per batch
LOG_BATCH_DELAY = 0.01 # Delay in seconds between batches (10ms)
display_batch_logs() (test_case.py:597-642, invoked from cleanup() when real-time logging is disabled) iterates the test case's output file and sleeps 10ms every 50 lines purely to yield to the event loop. For a real-world 673,988-line / 41MB output file, that's 13,480 batches × 10ms ≈ 135 seconds of pure asyncio.sleep, before any other work is counted. The same pacing constants are reused in _log_remaining_content() (test_case.py:578-584).
(Verified this is not caused by cross-test-case log accumulation: the SDK runner truncates the output file — open(EXECUTION_LOG_OUTPUT, "w", ...) in test_harness_client.py:204 — fresh for every test case.)
Compounding factors
-
Per-line log entry construction is not off-loaded.
backend/app/test_engine/test_log_handler.py:106-109registers thetest_run_logsink withoutenqueue=True, unlike the other two sinks inbackend/app/log.py:117,127. Every replayed line does synchronous Pydantic model construction inline on the run's coroutine. -
The entire run's log/state is committed to the DB exactly once, at the end of the run.
backend/app/test_engine/test_db_observer.py:53-57(apply_updates) has a single caller:backend/app/test_engine/test_runner.py:184, afterawait self.test_run.run()returns. This means a run's full log column (a single JSON list,backend/app/models/test_run_execution.py:76-82) is JSON-encoded and written in one largeUPDATEat the very end, rather than incrementally. -
CLI:
stream_sinkis missing theenqueue=Trueits own comment claims.cli/th_cli/test_run/logging.py:78-79:# Add sink with enqueue=True to prevent re-entrancy and catch=True to suppress errors logger.add(stream_sink, format="{message}", catch=True)enqueue=Trueis documented but not actually passed, sostream_sinkruns synchronously on the event loop per record. This also leaves open a real re-entrancy path:cli/th_cli/test_run/logs_http_server.py:163-164logs from the SSE-server thread every 100 entries sent, which re-entersstream_sink→ the same SSE queue. -
CLI: the yield interval never fires for the backend's actual batch size.
cli/th_cli/test_run/websocket.py:98,447-456:LOG_RECORD_YIELD_INTERVAL = 200 ... if (i + 1) % LOG_RECORD_YIELD_INTERVAL == 0: await asyncio.sleep(0)The backend sends
LOG_BATCH_SIZE = 50records per websocket message, so(i+1) % 200never hits for steady-state batches — the yield only helps for the rare oversized end-of-run flush message it wasn't primarily written for.
Suggested fixes
- Replace the fixed
LOG_BATCH_DELAY = 0.01pacing with a much larger batch size and a bareasyncio.sleep(0)yield (test_case.py:69-71,597-642,578-584). - Add
enqueue=Trueto thetest_run_logsink intest_log_handler.py:106-109, matching the pattern already used inlog.py:117,127. - Give
TestDBObserveran incremental flush loop (mirroringTestLogHandler's existingLOG_PROCESSING_INTERVALperiodic task) instead of relying solely on the single end-of-runapply_updates()call intest_runner.py:184, so the final commit isn't the entire run's log/state at once. - Pass
enqueue=Truetologger.add(stream_sink, ...)incli/th_cli/test_run/logging.py:79to match its own comment and remove the SSE-thread re-entrancy path throughlogs_http_server.py:163-164. - Lower
LOG_RECORD_YIELD_INTERVAL(or yield once per received message instead of per N records) incli/th_cli/test_run/websocket.py:98,447-456so it actually yields during steady-state batches sized at the backend'sLOG_BATCH_SIZE.
Environment / reproduction notes
Observed on a Raspberry Pi running a python_testing test run via the CLI (test-run-execution). Test steps complete quickly; the final "Show test logs" step (and/or the CLI's tail after the run reaches a terminal state) visibly stalls for a long time relative to actual execution time. Reproducible with any test case producing a large SDK log file (hundreds of thousands of lines).
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.