|
| 1 | +"""Tests for DPC log parser that extracts per-request timing from klog output.""" |
| 2 | + |
| 3 | +import pytest |
| 4 | + |
| 5 | +from workload.harnesses.dpc_log_parser import ( |
| 6 | + parse_dpc_log, |
| 7 | + parse_dpc_log_file, |
| 8 | +) |
| 9 | + |
| 10 | + |
| 11 | +# Representative klog lines from a DPC running with PR #522 at V(2) |
| 12 | +SAMPLE_LOG = """\ |
| 13 | +I0603 15:29:00.100000 1 inference-server.go:145] "Reconciling server" serverUID="abc-123" requesterName="fma-req-1-1717424983-abcde" |
| 14 | +I0603 15:29:01.200000 1 inference-server.go:1131] "Woke inference server" endpoint="http://10.0.0.1:8005/wake_up" description="discovered-bound" requesterName="fma-req-1-1717424983-abcde" httpCallStartTime="2026-06-03T15:29:01.150000000Z" |
| 15 | +I0603 15:29:03.500000 1 inference-server.go:513] "Successfully relayed the readiness" requesterName="fma-req-1-1717424983-abcde" name="launcher-27hdz" readiness="ready" url="http://10.0.0.2:8888/v1/become-ready" httpCallStartTime="2026-06-03T15:29:03.480000000Z" |
| 16 | +I0603 15:30:10.000000 1 inference-server.go:464] "Created vLLM instance" instance_id="inst-456" status="running" requesterName="fma-req-2-1717424983-fghij" httpCallStartTime="2026-06-03T15:30:09.900000000Z" |
| 17 | +I0603 15:30:50.000000 1 inference-server.go:513] "Successfully relayed the readiness" requesterName="fma-req-2-1717424983-fghij" name="launcher-27hdz" readiness="ready" url="http://10.0.0.3:8888/v1/become-ready" httpCallStartTime="2026-06-03T15:30:49.950000000Z" |
| 18 | +I0603 15:31:00.000000 1 inference-server.go:667] "Created launcher-based server-providing pod" name="launcher-xq8lg" gpus="GPU-abc" requesterName="fma-req-3-1717424983-klmno" k8sCallStartTime="2026-06-03T15:30:59.800000000Z" |
| 19 | +I0603 15:32:05.000000 1 inference-server.go:513] "Successfully relayed the readiness" requesterName="fma-req-3-1717424983-klmno" name="launcher-xq8lg" readiness="ready" url="http://10.0.0.4:8888/v1/become-ready" httpCallStartTime="2026-06-03T15:32:04.900000000Z" |
| 20 | +""" |
| 21 | + |
| 22 | + |
| 23 | +class TestParseDpcLog: |
| 24 | + """Test parse_dpc_log extracts timing records per requester.""" |
| 25 | + |
| 26 | + def test_hot_start_timing(self): |
| 27 | + records = parse_dpc_log(SAMPLE_LOG.splitlines()) |
| 28 | + rec = records["fma-req-1-1717424983-abcde"] |
| 29 | + assert rec.relay_readiness_time is not None |
| 30 | + assert rec.wake_start_time is not None |
| 31 | + assert rec.instance_create_start_time is None |
| 32 | + assert rec.launcher_create_start_time is None |
| 33 | + t_hot = rec.t_hot() |
| 34 | + assert t_hot is not None |
| 35 | + assert 2.3 < t_hot < 2.4 # 15:29:03.48 - 15:29:01.15 = 2.33s |
| 36 | + |
| 37 | + def test_warm_start_timing(self): |
| 38 | + records = parse_dpc_log(SAMPLE_LOG.splitlines()) |
| 39 | + rec = records["fma-req-2-1717424983-fghij"] |
| 40 | + assert rec.instance_create_start_time is not None |
| 41 | + assert rec.relay_readiness_time is not None |
| 42 | + assert rec.wake_start_time is None |
| 43 | + t_warm = rec.t_instance_create() |
| 44 | + assert t_warm is not None |
| 45 | + assert 40.0 < t_warm < 40.1 # 15:30:49.95 - 15:30:09.90 = 40.05s |
| 46 | + |
| 47 | + def test_cold_launcher_timing(self): |
| 48 | + records = parse_dpc_log(SAMPLE_LOG.splitlines()) |
| 49 | + rec = records["fma-req-3-1717424983-klmno"] |
| 50 | + assert rec.launcher_create_start_time is not None |
| 51 | + assert rec.relay_readiness_time is not None |
| 52 | + t_cold = rec.t_cold_launcher() |
| 53 | + assert t_cold is not None |
| 54 | + assert 65.0 < t_cold < 65.2 # 15:32:04.90 - 15:30:59.80 = 65.1s |
| 55 | + |
| 56 | + def test_unknown_requester_not_present(self): |
| 57 | + records = parse_dpc_log(SAMPLE_LOG.splitlines()) |
| 58 | + assert "nonexistent-pod" not in records |
| 59 | + |
| 60 | + def test_empty_log(self): |
| 61 | + records = parse_dpc_log([]) |
| 62 | + assert records == {} |
| 63 | + |
| 64 | + def test_log_with_no_timing_fields(self): |
| 65 | + records = parse_dpc_log( |
| 66 | + ['I0603 15:29:00.100000 1 foo.go:1] "Something unrelated"'] |
| 67 | + ) |
| 68 | + assert records == {} |
| 69 | + |
| 70 | + |
| 71 | +class TestParseDpcLogFile: |
| 72 | + """Test file-based entry point.""" |
| 73 | + |
| 74 | + def test_reads_log_file_and_parses(self, tmp_path): |
| 75 | + log_file = tmp_path / "dpctlr-pod--manager.log" |
| 76 | + log_file.write_text(SAMPLE_LOG) |
| 77 | + records = parse_dpc_log_file(str(tmp_path)) |
| 78 | + assert "fma-req-1-1717424983-abcde" in records |
| 79 | + assert records["fma-req-1-1717424983-abcde"].t_hot() is not None |
| 80 | + |
| 81 | + def test_missing_directory_returns_empty(self, tmp_path): |
| 82 | + records = parse_dpc_log_file(str(tmp_path / "nonexistent")) |
| 83 | + assert records == {} |
| 84 | + |
| 85 | + def test_no_matching_files_returns_empty(self, tmp_path): |
| 86 | + (tmp_path / "unrelated.log").write_text("nothing relevant here") |
| 87 | + records = parse_dpc_log_file(str(tmp_path)) |
| 88 | + assert records == {} |
| 89 | + |
| 90 | + def test_indicator_message_past_256kb_still_parsed(self, tmp_path): |
| 91 | + """A DPC log whose first indicator message lands past 256KB must still parse. |
| 92 | +
|
| 93 | + Regression for the original _is_dpc_log_file heuristic, which only sniffed |
| 94 | + the first 256KB. Real controller logs prepend large amounts of startup noise |
| 95 | + (flag dumps, leader election, reconcile churn) before the first |
| 96 | + relay/wake/create message, so the indicator can sit well past that boundary. |
| 97 | + """ |
| 98 | + filler_line = ( |
| 99 | + "I0603 15:00:00.000000 1 inference-server.go:145] " |
| 100 | + '"Reconciling server" serverUID="noise" requesterName="noise-pod"\n' |
| 101 | + ) |
| 102 | + # Prepend well over 256KB of benign filler, then the real sample block. |
| 103 | + filler = filler_line * (300 * 1024 // len(filler_line) + 1) |
| 104 | + assert len(filler) > 256 * 1024 |
| 105 | + log_file = tmp_path / "dpctlr-pod--manager.log" |
| 106 | + log_file.write_text(filler + SAMPLE_LOG) |
| 107 | + |
| 108 | + records = parse_dpc_log_file(str(tmp_path)) |
| 109 | + assert "fma-req-1-1717424983-abcde" in records |
| 110 | + assert records["fma-req-1-1717424983-abcde"].t_hot() is not None |
| 111 | + |
| 112 | + def test_file_without_relay_but_with_wake_still_parsed(self, tmp_path): |
| 113 | + """DPC log where requester crashed before relay should still be found.""" |
| 114 | + partial_log = ( |
| 115 | + 'I0603 15:29:01.000000 1 x.go:1] "Woke inference server" ' |
| 116 | + 'requesterName="req-crashed" httpCallStartTime="2026-06-03T15:29:00.900Z"\n' |
| 117 | + ) |
| 118 | + (tmp_path / "dpctlr--manager.log").write_text(partial_log) |
| 119 | + records = parse_dpc_log_file(str(tmp_path)) |
| 120 | + assert "req-crashed" in records |
| 121 | + assert records["req-crashed"].wake_start_time is not None |
| 122 | + assert records["req-crashed"].t_hot() is None # no relay = no duration |
| 123 | + |
| 124 | + |
| 125 | +class TestEdgeCases: |
| 126 | + """Edge cases for DPC log parsing robustness.""" |
| 127 | + |
| 128 | + def test_multiple_relay_readiness_uses_last(self): |
| 129 | + """If DPC retries readiness relay, use the last successful one.""" |
| 130 | + lines = [ |
| 131 | + 'I0603 15:29:01.000000 1 x.go:1] "Woke inference server" ' |
| 132 | + 'requesterName="req-retry" httpCallStartTime="2026-06-03T15:29:00.900Z"', |
| 133 | + 'I0603 15:29:03.000000 1 x.go:1] "Successfully relayed the readiness" ' |
| 134 | + 'requesterName="req-retry" readiness="ready" ' |
| 135 | + 'httpCallStartTime="2026-06-03T15:29:02.500Z"', |
| 136 | + 'I0603 15:29:05.000000 1 x.go:1] "Successfully relayed the readiness" ' |
| 137 | + 'requesterName="req-retry" readiness="ready" ' |
| 138 | + 'httpCallStartTime="2026-06-03T15:29:04.800Z"', |
| 139 | + ] |
| 140 | + records = parse_dpc_log(lines) |
| 141 | + rec = records["req-retry"] |
| 142 | + t_hot = rec.t_hot() |
| 143 | + assert t_hot is not None |
| 144 | + assert 3.8 < t_hot < 4.0 # 04.8 - 00.9 = 3.9s |
| 145 | + |
| 146 | + def test_unready_relay_ignored(self): |
| 147 | + """readiness='unready' lines should not set relay_readiness_time.""" |
| 148 | + lines = [ |
| 149 | + 'I0603 15:29:01.000000 1 x.go:1] "Woke inference server" ' |
| 150 | + 'requesterName="req-unready" httpCallStartTime="2026-06-03T15:29:00.900Z"', |
| 151 | + 'I0603 15:29:03.000000 1 x.go:1] "Successfully relayed the readiness" ' |
| 152 | + 'requesterName="req-unready" readiness="unready" ' |
| 153 | + 'httpCallStartTime="2026-06-03T15:29:02.500Z"', |
| 154 | + ] |
| 155 | + records = parse_dpc_log(lines) |
| 156 | + rec = records["req-unready"] |
| 157 | + assert rec.relay_readiness_time is None |
| 158 | + assert rec.t_hot() is None |
| 159 | + |
| 160 | + def test_malformed_timestamp_skipped(self): |
| 161 | + """Malformed httpCallStartTime should not crash, just skip.""" |
| 162 | + lines = [ |
| 163 | + 'I0603 15:29:01.000000 1 x.go:1] "Woke inference server" ' |
| 164 | + 'requesterName="req-bad" httpCallStartTime="not-a-timestamp"', |
| 165 | + 'I0603 15:29:03.000000 1 x.go:1] "Successfully relayed the readiness" ' |
| 166 | + 'requesterName="req-bad" readiness="ready" ' |
| 167 | + 'httpCallStartTime="2026-06-03T15:29:02.500Z"', |
| 168 | + ] |
| 169 | + records = parse_dpc_log(lines) |
| 170 | + rec = records["req-bad"] |
| 171 | + assert rec.wake_start_time is None |
| 172 | + assert rec.relay_readiness_time is not None |
| 173 | + |
| 174 | + def test_multiple_requesters_independent(self): |
| 175 | + """Different requesters get independent records.""" |
| 176 | + lines = [ |
| 177 | + 'I0603 15:29:01.000000 1 x.go:1] "Woke inference server" ' |
| 178 | + 'requesterName="req-a" httpCallStartTime="2026-06-03T15:29:00.000Z"', |
| 179 | + 'I0603 15:30:01.000000 1 x.go:1] "Created vLLM instance" ' |
| 180 | + 'requesterName="req-b" httpCallStartTime="2026-06-03T15:30:00.000Z"', |
| 181 | + 'I0603 15:29:05.000000 1 x.go:1] "Successfully relayed the readiness" ' |
| 182 | + 'requesterName="req-a" readiness="ready" ' |
| 183 | + 'httpCallStartTime="2026-06-03T15:29:04.000Z"', |
| 184 | + 'I0603 15:31:05.000000 1 x.go:1] "Successfully relayed the readiness" ' |
| 185 | + 'requesterName="req-b" readiness="ready" ' |
| 186 | + 'httpCallStartTime="2026-06-03T15:31:04.000Z"', |
| 187 | + ] |
| 188 | + records = parse_dpc_log(lines) |
| 189 | + assert records["req-a"].t_hot() == pytest.approx(4.0, abs=0.01) |
| 190 | + assert records["req-b"].t_instance_create() == pytest.approx(64.0, abs=0.01) |
| 191 | + assert records["req-a"].t_instance_create() is None |
| 192 | + assert records["req-b"].t_hot() is None |
0 commit comments