diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 9f6543c6c93..d97e01da71a 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -697,6 +697,10 @@ manifest: : bug (DEBUG-5101) tests/debugger/test_debugger_probe_status.py::Test_Debugger_Line_Probe_Statuses::test_probe_status_log_line_with_windows_path: bug (DEBUG-5108) tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses: v2.53.0 + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Coordinated_Sampling: missing_feature (no per-trace coordinated sampling; probabilistic per-trace rate not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Generation_Token: missing_feature (execution-context generation token not yet emitted) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Per_Span_Budget: missing_feature (per-span snapshot budget of 1 not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Runtime_Id_In_Envelope: missing_feature (runtime_id not yet present in the snapshot envelope) tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb: v2.53.0 tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb::test_event_metadata: v3.44.0 tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb::test_symdb_upload: # Modified by easy win activation script diff --git a/manifests/golang.yml b/manifests/golang.yml index 081c8c0f81f..6bfbc2c4673 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -1114,6 +1114,10 @@ manifest: tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses::test_metric_status: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses::test_span_decoration_method_status: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses::test_span_method_status: missing_feature (Not yet implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Coordinated_Sampling: missing_feature (no per-trace coordinated sampling; probabilistic per-trace rate not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Generation_Token: missing_feature (execution-context generation token not yet emitted) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Per_Span_Budget: missing_feature (per-span snapshot budget of 1 not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Runtime_Id_In_Envelope: missing_feature (runtime_id not yet present in the snapshot envelope) tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb: v2.2.3 # TODO(andrei): un-skip when datadog-agent v7.79.0 is released (Go's symdb upload comes from the agent, not from dd-trace-go) tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb::test_event_metadata: missing_feature (extended event schema not yet shipped) diff --git a/manifests/java.yml b/manifests/java.yml index 5bad17bdb4c..eac25b9c34c 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -3201,6 +3201,10 @@ manifest: spring-boot-undertow: v1.38.0 spring-boot-wildfly: v1.38.0 uds-spring-boot: v1.38.0 + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Coordinated_Sampling: missing_feature (no per-trace coordinated sampling; probabilistic per-trace rate not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Generation_Token: missing_feature (execution-context generation token not yet emitted) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Per_Span_Budget: missing_feature (per-span snapshot budget of 1 not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Runtime_Id_In_Envelope: missing_feature (runtime_id not yet present in the snapshot envelope) tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb: - weblog_declaration: "*": missing_feature diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index a0e0e432e1f..ce2db9848fd 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -1693,6 +1693,10 @@ manifest: tests/debugger/test_debugger_probe_status.py::Test_Debugger_Line_Probe_Statuses::test_metric_line_status: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_status.py::Test_Debugger_Line_Probe_Statuses::test_span_decoration_line_status: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses: missing_feature + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Coordinated_Sampling: missing_feature (no per-trace coordinated sampling; probabilistic per-trace rate not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Generation_Token: missing_feature (execution-context generation token not yet emitted) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Per_Span_Budget: missing_feature (per-span snapshot budget of 1 not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Runtime_Id_In_Envelope: missing_feature (runtime_id not yet present in the snapshot envelope) tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb: missing_feature (feature not implented) tests/debugger/test_debugger_telemetry.py::Test_Debugger_Telemetry: - weblog_declaration: diff --git a/manifests/php.yml b/manifests/php.yml index d6632fedc5a..2ebf243e5d2 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -721,6 +721,10 @@ manifest: apache-mod-8.0: v1.8.3 - declaration: flaky (DEBUG-3814) component_version: '>1.8.3' + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Coordinated_Sampling: missing_feature (no per-trace coordinated sampling; probabilistic per-trace rate not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Generation_Token: missing_feature (execution-context generation token not yet emitted) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Per_Span_Budget: missing_feature (per-span snapshot budget of 1 not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Runtime_Id_In_Envelope: missing_feature (runtime_id not yet present in the snapshot envelope) tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb: missing_feature tests/debugger/test_debugger_telemetry.py::Test_Debugger_Telemetry: - weblog_declaration: diff --git a/manifests/python.yml b/manifests/python.yml index 354b2993ec8..1e91a8e9a54 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -1317,6 +1317,10 @@ manifest: component_version: 2.16.0 - declaration: bug (DEBUG-3127) component_version: 2.16.1 + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Coordinated_Sampling: missing_feature (no per-trace coordinated sampling; probabilistic per-trace rate not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Generation_Token: missing_feature (execution-context generation token not yet emitted) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Per_Span_Budget: missing_feature (per-span snapshot budget of 1 not implemented; tracer uses a time-based budget) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Runtime_Id_In_Envelope: missing_feature (runtime_id not yet present in the snapshot envelope) tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb: - weblog_declaration: "*": missing_feature diff --git a/manifests/ruby.yml b/manifests/ruby.yml index e47be113875..59af20e3975 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -1872,6 +1872,10 @@ manifest: tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses::test_metric_status: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses::test_span_decoration_method_status: missing_feature (Not yet implemented) tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses::test_span_method_status: missing_feature (Not yet implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Coordinated_Sampling: missing_feature (no per-trace coordinated sampling; probabilistic per-trace rate not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Generation_Token: missing_feature (execution-context generation token not yet emitted) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Per_Span_Budget: missing_feature (per-span snapshot budget of 1 not implemented) + tests/debugger/test_debugger_snapshot_correlation.py::Test_Debugger_Runtime_Id_In_Envelope: missing_feature (runtime_id not yet present in the snapshot envelope) tests/debugger/test_debugger_symdb.py::Test_Debugger_SymDb: - weblog_declaration: "*": irrelevant diff --git a/tests/debugger/test_debugger_snapshot_correlation.py b/tests/debugger/test_debugger_snapshot_correlation.py new file mode 100644 index 00000000000..1d34e45583a --- /dev/null +++ b/tests/debugger/test_debugger_snapshot_correlation.py @@ -0,0 +1,267 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under the the Apache License Version 2.0. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2021 Datadog, Inc. + +import tests.debugger.utils as debugger + +from utils import scenarios, features, slow + +# Snapshot rate for the correlation probes, set through the tracer's existing per-probe sampling +# config (sampling.snapshotsPerSecond). It is the only sampling lever the DI probe config exposes: +# a per-probe rate limit in snapshots per second, not a probabilistic per-trace rate. Kept below +# 100% so some traces are sampled out. +_SNAPSHOTS_PER_SECOND = 1 + +# Envelope field carrying the tracer per-process runtime id. The snapshot envelope does not carry +# it today; the lookup tolerates the envelope root or the "dd" object. +_RUNTIME_ID_FIELD = "runtime_id" + +# Snapshot field carrying the execution-context generation token alongside thread_id. +_GENERATION_TOKEN_FIELD = "generation" + + +def _trace_id(snapshot: dict) -> str | None: + return snapshot.get("dd", {}).get("trace_id") + + +def _runtime_id(snapshot: dict) -> str | None: + return snapshot.get(_RUNTIME_ID_FIELD) or snapshot.get("dd", {}).get(_RUNTIME_ID_FIELD) + + +@features.debugger_snapshot_correlation +@scenarios.debugger_probes_snapshot +@slow +class Test_Debugger_Coordinated_Sampling(debugger.BaseDebuggerTest): + """Tier 1: the sampling decision is made once per trace and applies to every probe that + fires in that trace. This needs no change to the snapshot payload: correlation rides on the + dd.trace_id already present in the snapshot envelope, so the test just groups snapshots by + that trace_id. + + The call-chain endpoint spaces its probed call sites in time (real work between them), so the + probes in one trace are hit seconds apart rather than in lockstep. Under independent per-probe + sampling (the time-based per-probe rate limiter tracers have today) those spaced hits fall in + different rate windows, so a trace emits some probes and drops others: a partial chain. A + single per-trace decision instead keeps every probe in a trace together. So the discriminator + is: with spaced hits, only coordinated sampling yields zero partial chains. + + Assertions: zero partial chains, at least one fully emitted trace, at least one fully dropped + trace. There is no numeric emitted-fraction band because the only sampling lever the DI probe + config exposes is the time-based sampling.snapshotsPerSecond rate limit; a probabilistic + per-trace rate is not expressible, so a specific emitted fraction cannot be asserted. + + No current tracer makes a per-trace decision, so this is expected to xfail (gated + missing_feature for every language) until coordinated sampling ships. The spacing is what + keeps the xfail honest: without it, independent per-probe sampling could pass by lockstep + coincidence. + + Out of scope for this gate (not cleanly observable end to end through the weblog and + remote-config harness): Tier 2 task-context correlation when no distributed trace exists. + """ + + _INVOCATIONS = 30 + + def setup_coordinated_per_trace_sampling(self): + self.initialize_weblog_remote_config() + + probes = debugger.read_probes("probe_snapshot_log_correlation") + for probe in probes: + probe["id"] = debugger.generate_probe_id("log") + # Use the tracer's existing per-probe sampling config to sample below 100%. + probe["sampling"] = {"snapshotsPerSecond": _SNAPSHOTS_PER_SECOND} + self.set_probes(probes) + + self.send_rc_apm_tracing_and_probes(dynamic_instrumentation_enabled=True, dynamic_sampling_enabled=True) + if not self.wait_for_all_probes(statuses=["INSTALLED"], timeout=60): + self.setup_failures.append("Probes did not reach INSTALLED status") + return + + # Each request is one trace, so it is one sampling unit; the endpoint spaces the probe + # hits within the trace. + for i in range(self._INVOCATIONS): + self.send_weblog_request("/debugger/correlation", reset=(i == 0)) + + self.wait_for_all_probes(statuses=["EMITTING"]) + self.wait_for_all_snapshots() + + def test_coordinated_per_trace_sampling(self): + self.collect() + + self.assert_setup_ok() + self.assert_rc_state_not_error() + self.assert_all_weblog_responses_ok() + + chain = set(self.probe_ids) + + # Group the probes seen per trace using the existing dd.trace_id. A coordinated decision + # means every trace that emitted anything emitted the whole chain. + probes_by_trace: dict[str, set[str]] = {} + for probe_id in self.probe_ids: + for snapshot in self.probe_snapshots.get(probe_id, []): + trace_id = _trace_id(snapshot) + assert trace_id, f"snapshot for probe {probe_id} is missing dd.trace_id on the wire" + probes_by_trace.setdefault(trace_id, set()).add(probe_id) + + full = [trace_id for trace_id, probes in probes_by_trace.items() if probes == chain] + partial = [trace_id for trace_id, probes in probes_by_trace.items() if probes != chain] + # Fully dropped traces emit nothing and so never appear above. Every request is a distinct + # trace, so the emitted-trace count is the number of traces that produced any snapshot. + emitted = len(probes_by_trace) + dropped = self._INVOCATIONS - emitted + + assert not partial, ( + f"partial probe chains were emitted, the sampling decision is not made once per trace: {partial}" + ) + assert full, "no trace emitted the full probe chain" + assert dropped >= 1, "no trace was fully dropped; nothing was sampled out" + + +@features.debugger_probe_budgets +@scenarios.debugger_probes_snapshot +@slow +class Test_Debugger_Per_Span_Budget(debugger.BaseDebuggerTest): + """Each probe emits at most one snapshot per span execution. A probe inside a + high-iteration loop emits exactly one snapshot for the span and does not starve a + sibling probe on the same span, which still emits its one snapshot. + + This is stricter than the time-based budget in test_debugger_probe_budgets.py (which + tolerates 1 to 20 snapshots); the loop below runs long enough that a time-based budget + would emit more than one, so exactly one proves the cap is span scoped. + """ + + # The loop sleeps one second per iteration so the span lasts a few seconds: long enough + # that a time-based (per-second) budget would emit more than one snapshot, while a + # span-scoped budget emits exactly one. Kept below the weblog request timeout. + _LOOPS = 3 + + def setup_per_span_budget(self): + self.initialize_weblog_remote_config() + + language = self.get_tracer()["language"] + loop_lines = self.method_and_language_to_line_number("CorrelationLoopBody", language) + sibling_lines = self.method_and_language_to_line_number("CorrelationLoopSibling", language) + + template = debugger.read_probes("probe_snapshot_log_correlation_loop")[0] + probes = [] + for lines in (loop_lines, sibling_lines): + probe = {**template, "where": {**template["where"], "lines": [str(n) for n in lines]}} + probe["id"] = debugger.generate_probe_id("log") + probes.append(probe) + self.set_probes(probes) + + self.send_rc_probes() + if not self.wait_for_all_probes(statuses=["INSTALLED"], timeout=60): + self.setup_failures.append("Probes did not reach INSTALLED status") + return + + self.send_weblog_request(f"/debugger/correlation/loop/{self._LOOPS}") + self.wait_for_all_probes(statuses=["EMITTING"]) + self.wait_for_all_snapshots() + + def test_per_span_budget(self): + self.collect() + + self.assert_setup_ok() + self.assert_rc_state_not_error() + self.assert_all_weblog_responses_ok() + + # A single request is a single span, so every probe must have emitted exactly once. + for probe_id in self.probe_ids: + snapshots = [s for s in self.probe_snapshots.get(probe_id, []) if s.get("debugger", {}).get("snapshot")] + assert len(snapshots) == 1, ( + f"probe {probe_id} emitted {len(snapshots)} snapshots for one span, expected exactly 1" + ) + + +# Follow-on work, split out of feature 564 (which is coordinated sampling via trace_id only). +# There is no dashboard feature for this yet, and it is simply not implemented (not blocked on +# any schema negotiation), so it is not reported. +@features.not_reported +@scenarios.debugger_probes_snapshot +@slow +class Test_Debugger_Runtime_Id_In_Envelope(debugger.BaseDebuggerTest): + """Every snapshot envelope carries the tracer per-process runtime_id: present, non-empty, and + identical across all snapshots emitted by the same process. + """ + + def setup_runtime_id_in_envelope(self): + self.initialize_weblog_remote_config() + + probes = debugger.read_probes("probe_snapshot_log_correlation") + for probe in probes: + probe["id"] = debugger.generate_probe_id("log") + self.set_probes(probes) + + self.send_rc_probes() + if not self.wait_for_all_probes(statuses=["INSTALLED"], timeout=60): + self.setup_failures.append("Probes did not reach INSTALLED status") + return + + self.send_weblog_request("/debugger/correlation") + self.wait_for_all_probes(statuses=["EMITTING"]) + self.wait_for_all_snapshots() + + def test_runtime_id_in_envelope(self): + self.collect() + + self.assert_setup_ok() + self.assert_rc_state_not_error() + self.assert_all_weblog_responses_ok() + + runtime_ids = set() + for probe_id in self.probe_ids: + snapshots = self.probe_snapshots.get(probe_id, []) + assert snapshots, f"no snapshot was received for probe {probe_id}" + for snapshot in snapshots: + runtime_id = _runtime_id(snapshot) + assert runtime_id, f"snapshot for probe {probe_id} is missing a non-empty runtime_id" + runtime_ids.add(runtime_id) + + assert len(runtime_ids) == 1, f"expected one runtime_id across the run, got {runtime_ids}" + + +# Follow-on work, split out of feature 564 (which is coordinated sampling via trace_id only). +# There is no dashboard feature for this yet, and it is simply not implemented (not blocked on +# any schema negotiation), so it is not reported. +@features.not_reported +@scenarios.debugger_probes_snapshot +@slow +class Test_Debugger_Generation_Token(debugger.BaseDebuggerTest): + """Snapshots carry an execution-context generation token alongside thread_id so a reused + thread id can be disambiguated. Assert the token is present and non-empty. + """ + + def setup_generation_token(self): + self.initialize_weblog_remote_config() + + probes = debugger.read_probes("probe_snapshot_log_correlation") + for probe in probes: + probe["id"] = debugger.generate_probe_id("log") + self.set_probes(probes) + + self.send_rc_probes() + if not self.wait_for_all_probes(statuses=["INSTALLED"], timeout=60): + self.setup_failures.append("Probes did not reach INSTALLED status") + return + + self.send_weblog_request("/debugger/correlation") + self.wait_for_all_probes(statuses=["EMITTING"]) + self.wait_for_all_snapshots() + + def test_generation_token(self): + self.collect() + + self.assert_setup_ok() + self.assert_rc_state_not_error() + self.assert_all_weblog_responses_ok() + + for probe_id in self.probe_ids: + snapshots = self.probe_snapshots.get(probe_id, []) + assert snapshots, f"no snapshot was received for probe {probe_id}" + for snapshot in snapshots: + context = snapshot.get("logger", {}) + assert "thread_id" in context, f"snapshot for probe {probe_id} is missing thread_id" + token = context.get(_GENERATION_TOKEN_FIELD) + assert token is not None, ( + f"snapshot for probe {probe_id} is missing the execution-context generation token" + ) + assert token != "", f"snapshot for probe {probe_id} has an empty execution-context generation token" diff --git a/tests/debugger/utils.py b/tests/debugger/utils.py index 3ebba34bd82..ac3a62d5d98 100644 --- a/tests/debugger/utils.py +++ b/tests/debugger/utils.py @@ -168,6 +168,22 @@ def method_and_language_to_line_number(self, method: str, language: str) -> list "CollectionOperations": {"java": [114], "dotnet": [114], "python": [123], "ruby": [162], "nodejs": [120]}, "Nulls": {"java": [130], "dotnet": [127], "python": [136], "ruby": [192], "nodejs": [126]}, "SnapshotLimits": {"java": [153], "python": [172], "nodejs": [136], "ruby": [233], "dotnet": [150]}, + "CorrelationLoopBody": { + "python": [197], + "java": [177], + "dotnet": [181], + "nodejs": [148], + "ruby": [256], + "golang": [144], + }, + "CorrelationLoopSibling": { + "python": [199], + "java": [180], + "dotnet": [184], + "nodejs": [151], + "ruby": [259], + "golang": [147], + }, } return definitions.get(method, {}).get(language, []) @@ -229,6 +245,15 @@ def __get_probe_type(probe_id: str): method = probe["where"]["methodName"] method = method[0].lower() + method[1:] if method else "" probe["where"]["methodName"] = "main.(*DebuggerController)." + method + elif language == "nodejs": + # Node.js resolves a method probe by source file plus the function name + # (camelCase), not a class type name. + if context.weblog_variant == "express4-typescript": + probe["where"]["typeName"] = "debugger/index.ts" + else: + probe["where"]["typeName"] = "debugger/index.js" + method = probe["where"]["methodName"] + probe["where"]["methodName"] = method[0].lower() + method[1:] if method else "" elif probe["where"]["sourceFile"] == "ACTUAL_SOURCE_FILE": source_file = "" @@ -287,6 +312,8 @@ def __get_probe_type(probe_id: str): "20": "main.(*DebuggerController).logProbe", "71": "main.(*DebuggerController).expression", "117": "main.(*DebuggerController).budgetStep", + "144": "main.(*DebuggerController).correlationLoop", + "147": "main.(*DebuggerController).correlationLoop", } line = str(probe["where"]["lines"][0]) if line in golang_line_to_method: diff --git a/tests/debugger/utils/probes/probe_snapshot_log_correlation.json b/tests/debugger/utils/probes/probe_snapshot_log_correlation.json new file mode 100644 index 00000000000..df0803049b4 --- /dev/null +++ b/tests/debugger/utils/probes/probe_snapshot_log_correlation.json @@ -0,0 +1,38 @@ +[ + { + "language": "", + "type": "", + "id": "", + "version": 0, + "where": { + "typeName": "ACTUAL_TYPE_NAME", + "methodName": "Correlation", + "sourceFile": null + }, + "captureSnapshot": true + }, + { + "language": "", + "type": "", + "id": "", + "version": 0, + "where": { + "typeName": "ACTUAL_TYPE_NAME", + "methodName": "CorrelationMiddle", + "sourceFile": null + }, + "captureSnapshot": true + }, + { + "language": "", + "type": "", + "id": "", + "version": 0, + "where": { + "typeName": "ACTUAL_TYPE_NAME", + "methodName": "CorrelationLeaf", + "sourceFile": null + }, + "captureSnapshot": true + } +] diff --git a/tests/debugger/utils/probes/probe_snapshot_log_correlation_loop.json b/tests/debugger/utils/probes/probe_snapshot_log_correlation_loop.json new file mode 100644 index 00000000000..a547c04ed5f --- /dev/null +++ b/tests/debugger/utils/probes/probe_snapshot_log_correlation_loop.json @@ -0,0 +1,15 @@ +[ + { + "language": "", + "type": "", + "id": "", + "version": 0, + "where": { + "typeName": null, + "sourceFile": "ACTUAL_SOURCE_FILE", + "lines": [] + }, + "captureSnapshot": true, + "tags": [] + } +] diff --git a/utils/_features.py b/utils/_features.py index 86a15e3990f..8b83889dfc0 100644 --- a/utils/_features.py +++ b/utils/_features.py @@ -2337,6 +2337,19 @@ def debugger_probe_budgets(test_object): """ return _mark_test_object(test_object, feature_id=368, owner=_Owner.debugger) + @staticmethod + def debugger_snapshot_correlation(test_object): + """Coordinated per-trace snapshot sampling (Live Debugger) + + The sampling decision is made once per trace and applies to every probe in that trace, + with snapshots correlated by the dd.trace_id already carried in the snapshot envelope (no + snapshot payload change). Runtime-id-in-envelope and the execution-context generation + token are separate follow-on work and are not part of this feature. + + https://feature-parity.us1.prod.dog/#/?feature=564 + """ + return _mark_test_object(test_object, feature_id=564, owner=_Owner.debugger) + @staticmethod def debugger_method_probe(test_object): """Method-level Probes diff --git a/utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs b/utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs index c83db36f0a7..69bcd49f4a9 100644 --- a/utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs +++ b/utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs @@ -149,5 +149,40 @@ public IActionResult SnapshotLimits(int depth = 0, int collectionSize = 0, int s var longString = data["longString"]; return Content("Capture limits probe"); // must be line 150 } + + [HttpGet("correlation")] + [Consumes("application/json", "application/xml")] + public IActionResult Correlation() + { + int result = CorrelationMiddle(); + System.Threading.Thread.Sleep(400); // space the probed call sites in time + return Content($"Correlation {result}"); + } + + private int CorrelationMiddle() + { + int result = CorrelationLeaf(); + System.Threading.Thread.Sleep(400); // space the probed call sites in time + return result; + } + + private int CorrelationLeaf() + { + return 3; + } + + [HttpGet("correlation/loop/{loops}")] + [Consumes("application/json", "application/xml")] + public IActionResult CorrelationLoop(int loops) + { + int total = 0; + for (int i = 0; i < loops; i++) + { + total += i; + System.Threading.Thread.Sleep(1000); + } + int afterLoop = total; + return Content($"Loop {afterLoop}"); + } } } diff --git a/utils/build/docker/golang/app/chi/debugger.go b/utils/build/docker/golang/app/chi/debugger.go index 37a36a8fb80..f02170c58cc 100644 --- a/utils/build/docker/golang/app/chi/debugger.go +++ b/utils/build/docker/golang/app/chi/debugger.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "runtime" + "time" ) // The `debugger` feature allows attachment to specific lines of code. @@ -13,7 +14,6 @@ import ( type DebuggerController struct{} -// Dummy line // Dummy line func (d *DebuggerController) logProbe(w http.ResponseWriter, r *http.Request) { // Dummy line @@ -116,3 +116,35 @@ func (d *DebuggerController) budgets(w http.ResponseWriter, r *http.Request, loo func (d *DebuggerController) budgetStep(i, loops int) int { return i + loops // loops is referenced so the capture-expression probe can read it at this line. } + +//go:noinline +func (d *DebuggerController) correlation(w http.ResponseWriter, r *http.Request) { + result := d.correlationMiddle() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + runtime.KeepAlive(result) + w.Write([]byte(fmt.Sprintf("Correlation %d", result))) +} + +//go:noinline +func (d *DebuggerController) correlationMiddle() int { + result := d.correlationLeaf() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + return result +} + +//go:noinline +func (d *DebuggerController) correlationLeaf() int { + return 3 +} + +//go:noinline +func (d *DebuggerController) correlationLoop(w http.ResponseWriter, r *http.Request, loops int) { + total := 0 + for i := 0; i < loops; i++ { + total += i // correlation loop body line probe + time.Sleep(time.Second) + } + afterLoop := total // correlation loop sibling line probe + runtime.KeepAlive(afterLoop) + w.Write([]byte(fmt.Sprintf("Loop %d", afterLoop))) +} diff --git a/utils/build/docker/golang/app/chi/main.go b/utils/build/docker/golang/app/chi/main.go index 5e53068f360..b6b9b3fd070 100644 --- a/utils/build/docker/golang/app/chi/main.go +++ b/utils/build/docker/golang/app/chi/main.go @@ -421,6 +421,11 @@ func main() { loops, _ := strconv.Atoi(chi.RouteContext(r.Context()).URLParam("count")) d.budgets(w, r, loops) }) + mux.HandleFunc("/debugger/correlation", d.correlation) + mux.HandleFunc("/debugger/correlation/loop/{count}", func(w http.ResponseWriter, r *http.Request) { + loops, _ := strconv.Atoi(chi.RouteContext(r.Context()).URLParam("count")) + d.correlationLoop(w, r, loops) + }) srv := &http.Server{ Addr: ":7777", diff --git a/utils/build/docker/golang/app/echo/debugger.go b/utils/build/docker/golang/app/echo/debugger.go index 37a36a8fb80..f02170c58cc 100644 --- a/utils/build/docker/golang/app/echo/debugger.go +++ b/utils/build/docker/golang/app/echo/debugger.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "runtime" + "time" ) // The `debugger` feature allows attachment to specific lines of code. @@ -13,7 +14,6 @@ import ( type DebuggerController struct{} -// Dummy line // Dummy line func (d *DebuggerController) logProbe(w http.ResponseWriter, r *http.Request) { // Dummy line @@ -116,3 +116,35 @@ func (d *DebuggerController) budgets(w http.ResponseWriter, r *http.Request, loo func (d *DebuggerController) budgetStep(i, loops int) int { return i + loops // loops is referenced so the capture-expression probe can read it at this line. } + +//go:noinline +func (d *DebuggerController) correlation(w http.ResponseWriter, r *http.Request) { + result := d.correlationMiddle() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + runtime.KeepAlive(result) + w.Write([]byte(fmt.Sprintf("Correlation %d", result))) +} + +//go:noinline +func (d *DebuggerController) correlationMiddle() int { + result := d.correlationLeaf() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + return result +} + +//go:noinline +func (d *DebuggerController) correlationLeaf() int { + return 3 +} + +//go:noinline +func (d *DebuggerController) correlationLoop(w http.ResponseWriter, r *http.Request, loops int) { + total := 0 + for i := 0; i < loops; i++ { + total += i // correlation loop body line probe + time.Sleep(time.Second) + } + afterLoop := total // correlation loop sibling line probe + runtime.KeepAlive(afterLoop) + w.Write([]byte(fmt.Sprintf("Loop %d", afterLoop))) +} diff --git a/utils/build/docker/golang/app/echo/main.go b/utils/build/docker/golang/app/echo/main.go index f7fd2800660..7b99e868bfd 100644 --- a/utils/build/docker/golang/app/echo/main.go +++ b/utils/build/docker/golang/app/echo/main.go @@ -396,6 +396,12 @@ func main() { d.budgets(c.Response().Writer, c.Request(), loops) return nil }) + r.Any("/debugger/correlation", echoHandleFunc(d.correlation)) + r.Any("/debugger/correlation/loop/:count", func(c echo.Context) error { + loops, _ := strconv.Atoi(c.Param("count")) + d.correlationLoop(c.Response().Writer, c.Request(), loops) + return nil + }) common.InitDatadog() go grpc.ListenAndServe() diff --git a/utils/build/docker/golang/app/gin/debugger.go b/utils/build/docker/golang/app/gin/debugger.go index 37a36a8fb80..f02170c58cc 100644 --- a/utils/build/docker/golang/app/gin/debugger.go +++ b/utils/build/docker/golang/app/gin/debugger.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "runtime" + "time" ) // The `debugger` feature allows attachment to specific lines of code. @@ -13,7 +14,6 @@ import ( type DebuggerController struct{} -// Dummy line // Dummy line func (d *DebuggerController) logProbe(w http.ResponseWriter, r *http.Request) { // Dummy line @@ -116,3 +116,35 @@ func (d *DebuggerController) budgets(w http.ResponseWriter, r *http.Request, loo func (d *DebuggerController) budgetStep(i, loops int) int { return i + loops // loops is referenced so the capture-expression probe can read it at this line. } + +//go:noinline +func (d *DebuggerController) correlation(w http.ResponseWriter, r *http.Request) { + result := d.correlationMiddle() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + runtime.KeepAlive(result) + w.Write([]byte(fmt.Sprintf("Correlation %d", result))) +} + +//go:noinline +func (d *DebuggerController) correlationMiddle() int { + result := d.correlationLeaf() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + return result +} + +//go:noinline +func (d *DebuggerController) correlationLeaf() int { + return 3 +} + +//go:noinline +func (d *DebuggerController) correlationLoop(w http.ResponseWriter, r *http.Request, loops int) { + total := 0 + for i := 0; i < loops; i++ { + total += i // correlation loop body line probe + time.Sleep(time.Second) + } + afterLoop := total // correlation loop sibling line probe + runtime.KeepAlive(afterLoop) + w.Write([]byte(fmt.Sprintf("Loop %d", afterLoop))) +} diff --git a/utils/build/docker/golang/app/gin/main.go b/utils/build/docker/golang/app/gin/main.go index e1196e21bbc..4400ead63d4 100644 --- a/utils/build/docker/golang/app/gin/main.go +++ b/utils/build/docker/golang/app/gin/main.go @@ -381,6 +381,11 @@ func main() { loops, _ := strconv.Atoi(ctx.Param("count")) d.budgets(ctx.Writer, ctx.Request, loops) }) + r.Any("/debugger/correlation", ginHandleFunc(d.correlation)) + r.Any("/debugger/correlation/loop/:count", func(ctx *gin.Context) { + loops, _ := strconv.Atoi(ctx.Param("count")) + d.correlationLoop(ctx.Writer, ctx.Request, loops) + }) srv := &http.Server{ Addr: ":7777", diff --git a/utils/build/docker/golang/app/net-http-orchestrion/debugger.go b/utils/build/docker/golang/app/net-http-orchestrion/debugger.go index 37a36a8fb80..f02170c58cc 100644 --- a/utils/build/docker/golang/app/net-http-orchestrion/debugger.go +++ b/utils/build/docker/golang/app/net-http-orchestrion/debugger.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "runtime" + "time" ) // The `debugger` feature allows attachment to specific lines of code. @@ -13,7 +14,6 @@ import ( type DebuggerController struct{} -// Dummy line // Dummy line func (d *DebuggerController) logProbe(w http.ResponseWriter, r *http.Request) { // Dummy line @@ -116,3 +116,35 @@ func (d *DebuggerController) budgets(w http.ResponseWriter, r *http.Request, loo func (d *DebuggerController) budgetStep(i, loops int) int { return i + loops // loops is referenced so the capture-expression probe can read it at this line. } + +//go:noinline +func (d *DebuggerController) correlation(w http.ResponseWriter, r *http.Request) { + result := d.correlationMiddle() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + runtime.KeepAlive(result) + w.Write([]byte(fmt.Sprintf("Correlation %d", result))) +} + +//go:noinline +func (d *DebuggerController) correlationMiddle() int { + result := d.correlationLeaf() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + return result +} + +//go:noinline +func (d *DebuggerController) correlationLeaf() int { + return 3 +} + +//go:noinline +func (d *DebuggerController) correlationLoop(w http.ResponseWriter, r *http.Request, loops int) { + total := 0 + for i := 0; i < loops; i++ { + total += i // correlation loop body line probe + time.Sleep(time.Second) + } + afterLoop := total // correlation loop sibling line probe + runtime.KeepAlive(afterLoop) + w.Write([]byte(fmt.Sprintf("Loop %d", afterLoop))) +} diff --git a/utils/build/docker/golang/app/net-http-orchestrion/main.go b/utils/build/docker/golang/app/net-http-orchestrion/main.go index 965e1ac121f..dd111369c8a 100644 --- a/utils/build/docker/golang/app/net-http-orchestrion/main.go +++ b/utils/build/docker/golang/app/net-http-orchestrion/main.go @@ -643,6 +643,11 @@ func main() { loops, _ := strconv.Atoi(r.PathValue("count")) d.budgets(w, r, loops) }) + mux.HandleFunc("/debugger/correlation", d.correlation) + mux.HandleFunc("/debugger/correlation/loop/{count}", func(w http.ResponseWriter, r *http.Request) { + loops, _ := strconv.Atoi(r.PathValue("count")) + d.correlationLoop(w, r, loops) + }) srv := &http.Server{ Addr: ":7777", diff --git a/utils/build/docker/golang/app/net-http/debugger.go b/utils/build/docker/golang/app/net-http/debugger.go index 37a36a8fb80..f02170c58cc 100644 --- a/utils/build/docker/golang/app/net-http/debugger.go +++ b/utils/build/docker/golang/app/net-http/debugger.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "runtime" + "time" ) // The `debugger` feature allows attachment to specific lines of code. @@ -13,7 +14,6 @@ import ( type DebuggerController struct{} -// Dummy line // Dummy line func (d *DebuggerController) logProbe(w http.ResponseWriter, r *http.Request) { // Dummy line @@ -116,3 +116,35 @@ func (d *DebuggerController) budgets(w http.ResponseWriter, r *http.Request, loo func (d *DebuggerController) budgetStep(i, loops int) int { return i + loops // loops is referenced so the capture-expression probe can read it at this line. } + +//go:noinline +func (d *DebuggerController) correlation(w http.ResponseWriter, r *http.Request) { + result := d.correlationMiddle() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + runtime.KeepAlive(result) + w.Write([]byte(fmt.Sprintf("Correlation %d", result))) +} + +//go:noinline +func (d *DebuggerController) correlationMiddle() int { + result := d.correlationLeaf() + time.Sleep(400 * time.Millisecond) // space the probed call sites in time + return result +} + +//go:noinline +func (d *DebuggerController) correlationLeaf() int { + return 3 +} + +//go:noinline +func (d *DebuggerController) correlationLoop(w http.ResponseWriter, r *http.Request, loops int) { + total := 0 + for i := 0; i < loops; i++ { + total += i // correlation loop body line probe + time.Sleep(time.Second) + } + afterLoop := total // correlation loop sibling line probe + runtime.KeepAlive(afterLoop) + w.Write([]byte(fmt.Sprintf("Loop %d", afterLoop))) +} diff --git a/utils/build/docker/golang/app/net-http/main.go b/utils/build/docker/golang/app/net-http/main.go index 70b1bd5caf5..fd21a5e8436 100644 --- a/utils/build/docker/golang/app/net-http/main.go +++ b/utils/build/docker/golang/app/net-http/main.go @@ -784,6 +784,11 @@ func main() { loops, _ := strconv.Atoi(r.PathValue("count")) d.budgets(w, r, loops) }) + mux.HandleFunc("/debugger/correlation", d.correlation) + mux.HandleFunc("/debugger/correlation/loop/{count}", func(w http.ResponseWriter, r *http.Request) { + loops, _ := strconv.Atoi(r.PathValue("count")) + d.correlationLoop(w, r, loops) + }) srv := &http.Server{ Addr: ":7777", diff --git a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java index 0a87bea8924..32afac2e97d 100644 --- a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java +++ b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java @@ -152,4 +152,32 @@ public String snapshotLimits( String longString = (String) data.get("longString"); return "Capture limits probe"; // Line probe is instrumented here. } + + @GetMapping("/correlation") + public String correlation() throws InterruptedException { + int result = correlationMiddle(); + Thread.sleep(400); // space the probed call sites in time + return "Correlation " + result; + } + + private int correlationMiddle() throws InterruptedException { + int result = correlationLeaf(); + Thread.sleep(400); // space the probed call sites in time + return result; + } + + private int correlationLeaf() { + return 3; + } + + @GetMapping("/correlation/loop/{loops}") + public String correlationLoop(@PathVariable int loops) throws InterruptedException { + int total = 0; + for (int i = 0; i < loops; i++) { + total += i; + Thread.sleep(1000); + } + int afterLoop = total; + return "Loop " + afterLoop; + } } diff --git a/utils/build/docker/nodejs/express/debugger/index.js b/utils/build/docker/nodejs/express/debugger/index.js index 5374166b141..97c0d869f0c 100644 --- a/utils/build/docker/nodejs/express/debugger/index.js +++ b/utils/build/docker/nodejs/express/debugger/index.js @@ -135,5 +135,41 @@ module.exports = { }) res.send('Capture limits probe') // This needs to be line 136 }) + + app.get('/debugger/correlation', async (req, res) => { + const result = await correlation() + res.send(`Correlation ${result}`) + }) + + app.get('/debugger/correlation/loop/:loops', async (req, res) => { + const loops = parseInt(req.params.loops, 10) || 0 + let total = 0 + for (let i = 0; i < loops; i++) { + total += i + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + const afterLoop = total + res.send(`Loop ${afterLoop}`) + }) } } + +async function correlation () { + const result = await correlationMiddle() + await sleep(400) // space the probed call sites in time + return result +} + +async function correlationMiddle () { + const result = await correlationLeaf() + await sleep(400) // space the probed call sites in time + return result +} + +async function correlationLeaf () { + return 3 +} + +function sleep (ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/utils/build/docker/nodejs/express4-typescript/debugger/index.ts b/utils/build/docker/nodejs/express4-typescript/debugger/index.ts index 5c1d741a0f5..97412cc9894 100644 --- a/utils/build/docker/nodejs/express4-typescript/debugger/index.ts +++ b/utils/build/docker/nodejs/express4-typescript/debugger/index.ts @@ -135,4 +135,40 @@ export function initRoutes (app: Express) { }) res.send('Capture limits probe') // This needs to be line 136 }) + + app.get('/debugger/correlation', async (req: Request, res: Response) => { + const result = await correlation() + res.send(`Correlation ${result}`) + }) + + app.get('/debugger/correlation/loop/:loops', async (req: Request, res: Response) => { + const loops = parseInt(req.params.loops, 10) || 0 + let total = 0 + for (let i = 0; i < loops; i++) { + total += i + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + const afterLoop = total + res.send(`Loop ${afterLoop}`) + }) +} + +async function correlation (): Promise { + const result = await correlationMiddle() + await sleep(400) // space the probed call sites in time + return result +} + +async function correlationMiddle (): Promise { + const result = await correlationLeaf() + await sleep(400) // space the probed call sites in time + return result +} + +async function correlationLeaf (): Promise { + return 3 +} + +function sleep (ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) } diff --git a/utils/build/docker/nodejs/fastify/debugger/index.js b/utils/build/docker/nodejs/fastify/debugger/index.js index 3394abe7dcf..fcd44442960 100644 --- a/utils/build/docker/nodejs/fastify/debugger/index.js +++ b/utils/build/docker/nodejs/fastify/debugger/index.js @@ -135,5 +135,41 @@ module.exports = { }) return 'Capture limits probe' // This needs to be line 136 }) + + fastify.get('/debugger/correlation', async (request, reply) => { + const result = await correlation() + return `Correlation ${result}` + }) + + fastify.get('/debugger/correlation/loop/:loops', async (request, reply) => { + const loops = parseInt(request.params.loops, 10) || 0 + let total = 0 + for (let i = 0; i < loops; i++) { + total += i + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + const afterLoop = total + return `Loop ${afterLoop}` + }) } } + +async function correlation () { + const result = await correlationMiddle() + await sleep(400) // space the probed call sites in time + return result +} + +async function correlationMiddle () { + const result = await correlationLeaf() + await sleep(400) // space the probed call sites in time + return result +} + +async function correlationLeaf () { + return 3 +} + +function sleep (ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/utils/build/docker/python/flask/debugger/debugger_controller.py b/utils/build/docker/python/flask/debugger/debugger_controller.py index e0d54b5a4c7..941913c47f4 100644 --- a/utils/build/docker/python/flask/debugger/debugger_controller.py +++ b/utils/build/docker/python/flask/debugger/debugger_controller.py @@ -170,3 +170,42 @@ def snapshot_limits(): largeCollection = data["largeCollection"] # noqa: N806 longString = data["longString"] # noqa: N806 return "Capture limits probe", 200 + + +# The endpoints below are appended so the line numbers of the endpoints above +# stay stable for the line-probe tests that reference them. +@debugger_blueprint.route("/correlation", methods=["GET"]) +def correlation(): + result = correlation_spin(correlation_middle()) + return f"Correlation {result}", 200 + + +def correlation_middle(): + return correlation_spin(correlation_leaf()) + + +def correlation_leaf(): + return 3 + + +@debugger_blueprint.route("/correlation/loop/", methods=["GET"]) +def correlation_loop(loops): + import time + + total = 0 + for i in range(loops): + total += i # correlation loop body line probe + time.sleep(1) + afterLoop = total # correlation loop sibling line probe # noqa: N806 + return f"Loop {afterLoop}", 200 + + +def correlation_spin(value): + # Space the probed call sites of the correlation chain in time. Under independent per-probe + # sampling this makes some probes in a trace emit while others drop (partial chains), whereas + # a single per-trace decision keeps the whole chain together. Defined after the loop endpoint + # so it does not shift the line numbers the line-probe tests reference. + import time + + time.sleep(0.4) + return value diff --git a/utils/build/docker/ruby/rails72/config/routes.rb b/utils/build/docker/ruby/rails72/config/routes.rb index 861ebb5513d..c2ba7f68a32 100644 --- a/utils/build/docker/ruby/rails72/config/routes.rb +++ b/utils/build/docker/ruby/rails72/config/routes.rb @@ -70,6 +70,8 @@ def call(_env) get '/debugger/init' => 'debugger#init' get '/debugger/pii' => 'debugger#pii' get '/debugger/log' => 'debugger#log_probe' + get '/debugger/correlation' => 'debugger#correlation' + get '/debugger/correlation/loop/:loops' => 'debugger#correlation_loop' get '/debugger/mix/:string_arg/:int_arg' => 'debugger#mix_probe' get '/debugger/snapshot/limits' => 'debugger#snapshot_limits' get '/debugger/expression' => 'debugger#expression' diff --git a/utils/build/docker/ruby/rails80/config/routes.rb b/utils/build/docker/ruby/rails80/config/routes.rb index 9d53bd6d8aa..e1d651f5006 100644 --- a/utils/build/docker/ruby/rails80/config/routes.rb +++ b/utils/build/docker/ruby/rails80/config/routes.rb @@ -62,6 +62,8 @@ def call(_env) get '/debugger/init' => 'debugger#init' get '/debugger/pii' => 'debugger#pii' get '/debugger/log' => 'debugger#log_probe' + get '/debugger/correlation' => 'debugger#correlation' + get '/debugger/correlation/loop/:loops' => 'debugger#correlation_loop' get '/debugger/mix/:string_arg/:int_arg' => 'debugger#mix_probe' get '/debugger/snapshot/limits' => 'debugger#snapshot_limits' get '/debugger/expression' => 'debugger#expression' diff --git a/utils/build/docker/ruby/shared/rails/app/controllers/debugger_controller.rb b/utils/build/docker/ruby/shared/rails/app/controllers/debugger_controller.rb index 12bcd4fbc6d..1e875e22751 100644 --- a/utils/build/docker/ruby/shared/rails/app/controllers/debugger_controller.rb +++ b/utils/build/docker/ruby/shared/rails/app/controllers/debugger_controller.rb @@ -232,4 +232,31 @@ def snapshot_limits longString = data[:longString] render inline: 'Capture limits probe' # must be line 233 end + + def correlation + result = correlation_middle + sleep 0.4 # space the probed call sites in time + render inline: "Correlation #{result}" + end + + def correlation_middle + result = correlation_leaf + sleep 0.4 # space the probed call sites in time + result + end + + def correlation_leaf + 3 + end + + def correlation_loop + loops = params[:loops].to_i + total = 0 + loops.times do |i| + total += i + sleep 1 + end + after_loop = total + render inline: "Loop #{after_loop}" + end end