Skip to content

Extract OffloadingService and test the inference hot path (#45)#77

Open
ff225 wants to merge 1 commit into
fix/sciot-010-inference-cycle-recorderfrom
fix/sciot-045-offloading-service
Open

Extract OffloadingService and test the inference hot path (#45)#77
ff225 wants to merge 1 commit into
fix/sciot-010-inference-cycle-recorderfrom
fix/sciot-045-offloading-service

Conversation

@ff225

@ff225 ff225 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #45. Third and last step, after #74 (state) and #76 (recorder).

Stacked on #76 — base is fix/sciot-010-inference-cycle-recorder, not main. Review #74#76 → this. Independent of #75.

The #45 checklist

OffloadingService

RequestHandler built the algorithm, ran it, unpicked its candidate list and published the decision telemetry inline. OffloadingService owns that now, returning an OffloadingDecision that carries everything telemetry needs to explain the choice: the layer, the reason, the candidates, the switch penalty and the estimated cost.

Deriving the switch penalty and the cost estimate moves onto the decision object — they are properties of the decision, not of the request. The handler is left orchestrating: it asks for a decision and reports it.

Deliberate deviation from the issue. #45 says "OffloadingService (wrapper around OffloadingAlgo + decision logging)". The service owns the algorithm and the decision logging (append_and_publish_decision), but building the telemetry events stays in the handler: those need message_data, and moving them would couple the service to the transport message shape. Happy to move them if you disagree.

The algorithms are untouched. src/server/offloading_algo/ has zero changes in this PR — only the caller moved. Same OffloadingContext, same create_offloading_algorithm, same select_offloading_layer().

Tests for the hot path

#45 called handle_device_inference_result "the most critical component [with] no dedicated tests". tests/unit/test_handle_device_inference_result.py now covers:

  • the EMA update for device layers and for edge layers (correctly offset past the offloading point), and that untouched layers keep their seed;
  • variance measurements landing in the per-device detector;
  • both edge-inference skip conditions (offloading_layer_index == -1, and past ultimo_layer);
  • the offloading decision: the layer is returned, forced-local bypasses the algorithm entirely, the reported network speed is passed through, the previous speed is kept when the packet reports none, and the decision sees the freshly smoothed times rather than the pre-update ones;
  • background I/O scheduling for every output, with each queued task actually executed and its effect asserted;
  • that the debug snapshot is immune to later mutation of the live tables — the race Extract per-device state out of RequestHandler (#11) #74's snapshot exists to prevent;
  • that telemetry is skipped entirely when both outputs are disabled.

The handler is built with object.__new__, so none of the heavy __init__ (profiler, config loading, Firestore) runs.

Result

before after
request_handler.py 1158 lines 933
handle_device_inference_result 322 lines 279
request_handler.py coverage 48% 61%

287 passed, 1 skipped. device_state.py, inference_recorder.py and offloading_service.py all at 100% coverage. ruff clean.

Being straight about what is left: the handler is meaningfully thinner and its state is now owned elsewhere, but handle_device_inference_result is still 279 lines — the bulk is telemetry event assembly. Every box on #45 is ticked and the god-class state problem is solved, but if you want that method genuinely short, extracting an event-builder is a natural fourth step. Say the word and I will file it.

Note

Two tests moved out of test_request_handler_helpers.py into tests/unit/test_offloading_service.py, since they targeted methods that now live on the service. They gained coverage of branches nothing exercised before: the IndexError metadata fallback, forced-local, and the switch_penalty derivation (including candidates excluded from selection).

…t path (#45)

Third and last step of #45.

RequestHandler built the offloading algorithm, ran it, unpicked its candidate
list and published the decision telemetry inline. OffloadingService now owns
that, and returns an OffloadingDecision carrying everything telemetry needs to
explain the choice: the layer, the reason, the candidates, the switch penalty
and the estimated cost.

Deriving the switch penalty and the cost estimate moves onto the decision
object, where they belong — they are properties of the decision, not of the
request. The handler is left orchestrating: it asks for a decision and reports
it.

Building the telemetry *events* stays in the handler on purpose: those need
`message_data`, and moving them would couple the service to the transport
message shape.

The algorithms themselves are untouched: `src/server/offloading_algo/` has no
changes. Only the caller moved.

Adds tests for handle_device_inference_result, which #45 flagged as the most
critical component with no dedicated tests: the EMA updates for device and edge
layers, the variance recording, the edge-inference skip conditions, the
offloading decision (including forced-local, and that the decision sees the
freshly smoothed times), the background I/O scheduling for every output, and
that the debug snapshot is immune to later mutation of the live tables.

request_handler.py: 1158 -> 933 lines; coverage 48% -> 61%.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant