mod-wsgi-telemetry 1.0.0.dev2__tar.gz → 1.0.0.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/PKG-INFO +1 -1
  2. mod_wsgi_telemetry-1.0.0.dev3/src/mod_wsgi/telemetry/__init__.py +1 -0
  3. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/static/index.html +117 -32
  4. mod_wsgi_telemetry-1.0.0.dev2/src/mod_wsgi/telemetry/__init__.py +0 -1
  5. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/.gitignore +0 -0
  6. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/LICENSE +0 -0
  7. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/README-telemetry.rst +0 -0
  8. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/README.md +0 -0
  9. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/pyproject.toml +0 -0
  10. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/__init__.py +0 -0
  11. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/cli.py +0 -0
  12. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/contention.py +0 -0
  13. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/dump.py +0 -0
  14. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/ingest.py +0 -0
  15. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/server.py +0 -0
  16. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/simulate.py +0 -0
  17. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/tui.py +0 -0
  18. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/src/mod_wsgi/telemetry/wire.py +0 -0
  19. {mod_wsgi_telemetry-1.0.0.dev2 → mod_wsgi_telemetry-1.0.0.dev3}/tests/test_wire.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mod_wsgi-telemetry
3
- Version: 1.0.0.dev2
3
+ Version: 1.0.0.dev3
4
4
  Summary: Ingestion service and live UI for mod_wsgi telemetry samples.
5
5
  Project-URL: Homepage, https://www.modwsgi.org/
6
6
  Project-URL: Documentation, https://modwsgi.readthedocs.io/en/latest/user-guides/external-telemetry-service.html
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0.dev3"
@@ -1206,8 +1206,8 @@
1206
1206
  <div class="gc-status" id="gc_status"></div>
1207
1207
  <div class="charts">
1208
1208
  <div class="chart">
1209
- <div class="header-row"><h3>Generation pressure</h3></div>
1210
- <div class="header-row"><div class="unit">top: gen2 raw count[2] (gen1 collections since last full sweep); bottom: gen0/gen1 count / threshold (1.0 = collection imminent)</div><div class="live" id="live_gc_pressure"></div></div>
1209
+ <div class="header-row"><h3>GC overhead</h3></div>
1210
+ <div class="header-row"><div class="unit">per-interval fraction of wall-clock spent in GC pauses (sum of pause durations / interval length); invariant across CPython GC algorithms</div><div class="live" id="live_gc_pressure"></div></div>
1211
1211
  <canvas id="c_gc_pressure"></canvas>
1212
1212
  </div>
1213
1213
  <div class="chart">
@@ -5494,6 +5494,96 @@ function gcDrawLines(canvas, series, extract, opts = {}) {
5494
5494
  }
5495
5495
  }
5496
5496
 
5497
+ function gcDrawOverhead(canvas, series, opts = {}) {
5498
+ // Per (pid, interpreter) series: one line whose y-value at each
5499
+ // snapshot is the fraction of wall-clock time spent inside GC
5500
+ // pauses over the interval ending at that snapshot, expressed as
5501
+ // a percentage. Interval is [prev_snapshot.t, this_snapshot.t];
5502
+ // events are binned by gc_event_start_stamp (the collection's
5503
+ // actual start time) and fall back to the datagram stamp when
5504
+ // that field is absent.
5505
+ //
5506
+ // Same calculation in every CPython GC regime (pre-3.13
5507
+ // generational, 3.14.0-3.14.4 incremental, 3.14.5 reverted,
5508
+ // free-threaded), so a chart that aggregates across mixed-build
5509
+ // process groups stays honest. No gc.get_count() / threshold
5510
+ // arithmetic is involved.
5511
+ const ctx = canvas.getContext("2d");
5512
+ const w = canvas.width = canvas.clientWidth * devicePixelRatio;
5513
+ const h = canvas.height = canvas.clientHeight * devicePixelRatio;
5514
+ ctx.clearRect(0, 0, w, h);
5515
+
5516
+ const now = gcNowSec();
5517
+ const windowSec = opts.windowSec || LINE_WINDOW_SEC;
5518
+ const tmin = now - windowSec;
5519
+ const tmax = now;
5520
+
5521
+ let vmax = 0;
5522
+ const lines = [];
5523
+ for (const s of series) {
5524
+ const ring = s.snapshots.filter(p => p.t >= tmin);
5525
+ if (ring.length < 2) continue;
5526
+ const evs = s.events
5527
+ .map(e => ({
5528
+ t: e.fields.gc_event_start_stamp || e.t,
5529
+ d: e.fields.gc_event_duration || 0,
5530
+ }))
5531
+ .sort((a, b) => a.t - b.t);
5532
+ let ei = 0;
5533
+ const pts = [];
5534
+ for (let i = 1; i < ring.length; i++) {
5535
+ const t0 = ring[i - 1].t;
5536
+ const t1 = ring[i].t;
5537
+ const dt = t1 - t0;
5538
+ if (dt <= 0) continue;
5539
+ while (ei < evs.length && evs[ei].t < t0) ei++;
5540
+ let sum = 0;
5541
+ let j = ei;
5542
+ while (j < evs.length && evs[j].t < t1) {
5543
+ sum += evs[j].d;
5544
+ j++;
5545
+ }
5546
+ const pct = (sum / dt) * 100;
5547
+ if (pct > vmax) vmax = pct;
5548
+ pts.push([t1, pct]);
5549
+ }
5550
+ if (pts.length === 0) continue;
5551
+ lines.push({ color: s.color, pts, label: s.label });
5552
+ }
5553
+ if (vmax === 0) vmax = opts.fallbackMax || 1;
5554
+ vmax *= 1.1;
5555
+
5556
+ ctx.strokeStyle = "#333";
5557
+ ctx.lineWidth = 1;
5558
+ ctx.beginPath();
5559
+ ctx.moveTo(0, h - 1);
5560
+ ctx.lineTo(w, h - 1);
5561
+ ctx.stroke();
5562
+
5563
+ ctx.fillStyle = "#666";
5564
+ ctx.font = (10 * devicePixelRatio) + "px -apple-system, sans-serif";
5565
+ ctx.textAlign = "left";
5566
+ ctx.textBaseline = "top";
5567
+ ctx.fillText(vmax.toFixed(2) + "%", 4, 4);
5568
+ ctx.textBaseline = "bottom";
5569
+ ctx.fillText("0%", 4, h - 4);
5570
+
5571
+ const xOf = t => ((t - tmin) / (tmax - tmin)) * w;
5572
+ const yOf = v => h - (v / vmax) * h;
5573
+
5574
+ ctx.lineWidth = 1.5 * devicePixelRatio;
5575
+ for (const l of lines) {
5576
+ ctx.strokeStyle = l.color;
5577
+ ctx.beginPath();
5578
+ l.pts.forEach(([t, v], i) => {
5579
+ const x = xOf(t), y = yOf(v);
5580
+ if (i === 0) ctx.moveTo(x, y);
5581
+ else ctx.lineTo(x, y);
5582
+ });
5583
+ ctx.stroke();
5584
+ }
5585
+ }
5586
+
5497
5587
  function gcDrawEventTimeline(canvas, series, windowSec, opts = {}) {
5498
5588
  // opts.gens filters which generations contribute dots; opts.region
5499
5589
  // and opts.skipClear behave the same as gcDrawLines so the caller
@@ -5698,37 +5788,32 @@ function renderGc() {
5698
5788
 
5699
5789
  renderGcStatus(series);
5700
5790
 
5701
- // Generation pressure: split vertically so the rare-but-large gen2
5702
- // sawtooth (top, raw count[2] = gen1 collections since the last full
5703
- // sweep) doesn't squash the gen0/gen1 strip (bottom, normalized to
5704
- // threshold so 1.0 = collection imminent). Layout matches the GC
5705
- // event timeline below: gen2 on top, gen0/gen1 on bottom. Gen2 uses
5706
- // a long-lived-pending heuristic for its trigger, so count/threshold
5707
- // isn't a meaningful normalization for it; raw count[2] is plotted
5708
- // on its own y-axis instead.
5791
+ // GC overhead: per (pid, interpreter) series, fraction of wall-clock
5792
+ // spent in GC pauses over each snapshot interval, as a percentage.
5793
+ // Built from the per-event durations in the gc_event stream, so the
5794
+ // calculation is identical across CPython GC algorithms (pre-3.13
5795
+ // generational, 3.14.0-3.14.4 incremental, 3.14.5 reverted, free-
5796
+ // threaded). No reliance on gc.get_count() / threshold semantics,
5797
+ // which diverge between those algorithms.
5709
5798
  const pressureCanvas = document.getElementById("c_gc_pressure");
5710
- gcDrawLines(pressureCanvas, series,
5711
- (fields, _prev, _dt, _gen) => {
5712
- const cnt = fields.gc_count2;
5713
- return cnt === undefined ? null : cnt;
5714
- }, {
5715
- fallbackMax: 1, precision: 0, windowSec, gens: [2],
5716
- region: { top: 0.0, bottom: 0.30 },
5717
- });
5718
- gcDrawLines(pressureCanvas, series,
5719
- (fields, _prev, _dt, gen) => {
5720
- const thr = fields["gc_threshold" + gen];
5721
- const cnt = fields["gc_count" + gen];
5722
- if (!thr || cnt === undefined) return null;
5723
- return cnt / thr;
5724
- }, {
5725
- fallbackMax: 1.0, precision: 2, windowSec, gens: [0, 1],
5726
- region: { top: 0.34, bottom: 1.0 }, skipClear: true,
5727
- });
5728
- setLive("live_gc_pressure",
5729
- `<span class="label" style="color:${GC_GEN_COLORS[0]}">gen0</span> ` +
5730
- `<span class="label" style="color:${GC_GEN_COLORS[1]}">gen1</span> ` +
5731
- `<span class="label" style="color:${GC_GEN_COLORS[2]}">gen2</span>`);
5799
+ gcDrawOverhead(pressureCanvas, series, { windowSec });
5800
+ // Live readout: per-series window-average overhead %, coloured to
5801
+ // match each line on the chart.
5802
+ const overheadWinFloor = gcNowSec() - windowSec;
5803
+ const overheadLabels = [];
5804
+ for (const s of series) {
5805
+ let sum = 0;
5806
+ for (const e of s.events) {
5807
+ const t = e.fields.gc_event_start_stamp || e.t;
5808
+ if (t < overheadWinFloor) continue;
5809
+ sum += e.fields.gc_event_duration || 0;
5810
+ }
5811
+ const pct = (sum / windowSec) * 100;
5812
+ overheadLabels.push(
5813
+ `<span class="label" style="color:${s.color}">${s.label}</span>` +
5814
+ `${pct.toFixed(2)}%`);
5815
+ }
5816
+ setLive("live_gc_pressure", overheadLabels.join(" "));
5732
5817
 
5733
5818
  // Collections per second: rate of cumulative gc_collections* over the
5734
5819
  // delta between successive snapshots.
@@ -1 +0,0 @@
1
- __version__ = "1.0.0dev2"