|
40 | 40 | var PAN_THROTTLE_MS = 50; // pan/zoom throttle — looser than slider |
41 | 41 | var FETCH_N = "all"; // lazy-fetch the entire raw history |
42 | 42 | var DEFAULT_VISIBLE = 100; // initial visible window (last 100 of fetched) |
| 43 | + // Mirror of `LANDING_INLINE_N` in `server/src/html.rs`. The first group's |
| 44 | + // inline JSON is capped at this many commits to keep the cold landing |
| 45 | + // page small. When the user zooms wider than what's inlined we lazy-fetch |
| 46 | + // `?n=all` and replace the payload in place. If you change this, update |
| 47 | + // the server too — the comparison `commits.length >= LANDING_INLINE_N` |
| 48 | + // is what tells us the inline payload was potentially trimmed. |
| 49 | + var LANDING_INLINE_N = 100; |
43 | 50 | // Hard cap on how many points a single series can render at once. When |
44 | 51 | // the visible commit range has more raw non-null points than this, we |
45 | 52 | // LTTB-downsample to exactly this number; below it we render raw. So |
|
617 | 624 | chart.update("none"); |
618 | 625 | syncSliderFromRange(card, visibleCommits); |
619 | 626 | syncDownsampleBadge(card, keptCommits, visibleCommits, anyDownsampled); |
| 627 | + // If the user has zoomed out to cover everything we have inlined and the |
| 628 | + // server might have more commits, fetch the full history in the |
| 629 | + // background. The `__bench_full_loaded` / `__bench_full_fetch_pending` |
| 630 | + // flags dedupe so this fires once per chart even when called every |
| 631 | + // pan frame. |
| 632 | + maybeRefetchFullPayload(card, min, max, n); |
| 633 | + } |
| 634 | + |
| 635 | + // ----------------------------------------------------------------------- |
| 636 | + // Lazy-upgrade an inline-trimmed payload to the full history. |
| 637 | + // |
| 638 | + // The landing page inlines at most `LANDING_INLINE_N` commits per chart |
| 639 | + // (server: `html.rs::LANDING_INLINE_N`) so the cold HTML body stays small. |
| 640 | + // The first time the user zooms wide enough to ask for everything we have |
| 641 | + // loaded we replace the payload with the unbounded view from |
| 642 | + // `/api/chart/{slug}?n=all`. The chart's pan/zoom limits and the toolbar |
| 643 | + // slider's max grow to match, so subsequent zoom-out passes can scroll |
| 644 | + // back through the older commits the inline payload didn't include. |
| 645 | + // ----------------------------------------------------------------------- |
| 646 | + function maybeRefetchFullPayload(card, min, max, loadedCount) { |
| 647 | + var canvas = card.querySelector("canvas"); |
| 648 | + if (!canvas) return; |
| 649 | + if (!canvas.__bench_inline_trimmed) return; |
| 650 | + if (canvas.__bench_full_loaded || canvas.__bench_full_fetch_pending) return; |
| 651 | + // Trigger only when the visible range covers (effectively) every loaded |
| 652 | + // commit. Anything narrower means the user hasn't asked for "more" |
| 653 | + // yet — there's no reason to spend bandwidth on a refetch they don't |
| 654 | + // need. |
| 655 | + if (loadedCount <= 0) return; |
| 656 | + var coversAll = (max - min + 1) >= loadedCount; |
| 657 | + if (!coversAll) return; |
| 658 | + canvas.__bench_full_fetch_pending = true; |
| 659 | + var slug = card.getAttribute("data-chart-slug"); |
| 660 | + if (!slug) { |
| 661 | + canvas.__bench_full_fetch_pending = false; |
| 662 | + return; |
| 663 | + } |
| 664 | + var url = "/api/chart/" + encodeURIComponent(slug) |
| 665 | + + "?n=" + encodeURIComponent(FETCH_N); |
| 666 | + fetch(url, { headers: { "accept": "application/json" } }) |
| 667 | + .then(function (r) { |
| 668 | + if (r.status === 404) return null; |
| 669 | + if (!r.ok) throw new Error("HTTP " + r.status); |
| 670 | + return r.json(); |
| 671 | + }) |
| 672 | + .then(function (full) { |
| 673 | + if (!full) return; |
| 674 | + replaceChartPayload(card, full); |
| 675 | + canvas.__bench_full_loaded = true; |
| 676 | + canvas.__bench_inline_trimmed = false; |
| 677 | + }) |
| 678 | + .catch(function (err) { |
| 679 | + // Quiet — the inline payload is still rendered, the user just |
| 680 | + // can't zoom past it. Surface to the console for debugging. |
| 681 | + if (window && window.console) { |
| 682 | + window.console.warn("bench: full history refetch failed", err); |
| 683 | + } |
| 684 | + }) |
| 685 | + .then(function () { |
| 686 | + canvas.__bench_full_fetch_pending = false; |
| 687 | + }); |
| 688 | + } |
| 689 | + |
| 690 | + // Swap the chart's labels + datasets to a freshly fetched, unbounded |
| 691 | + // payload while keeping the user's currently visible commit window |
| 692 | + // anchored on the *newest* commit. The pan/zoom limits and toolbar |
| 693 | + // slider bounds are extended to the new total commit count. |
| 694 | + function replaceChartPayload(card, payload) { |
| 695 | + var canvas = card.querySelector("canvas"); |
| 696 | + var chart = canvas && canvas.__bench_chart; |
| 697 | + if (!chart || !payload) return; |
| 698 | + canvas.__bench_payload = payload; |
| 699 | + var newLabels = (payload.commits || []).map(function (c) { |
| 700 | + return shortSha(c.sha); |
| 701 | + }); |
| 702 | + var newDatasets = buildDatasets(payload); |
| 703 | + // Re-apply per-card legend overrides + global filter to the new datasets, |
| 704 | + // matching the visibility state the user had before the refetch. |
| 705 | + var overrides = canvas.__bench_overrides || {}; |
| 706 | + for (var i = 0; i < newDatasets.length; i++) { |
| 707 | + var ds = newDatasets[i]; |
| 708 | + if (overrides[ds.label]) { |
| 709 | + // Honour any explicit legend toggle the user had made already. |
| 710 | + var prev = chart.data.datasets.find(function (p) { |
| 711 | + return p.label === ds.label; |
| 712 | + }); |
| 713 | + if (prev) ds.hidden = !!prev.hidden; |
| 714 | + } |
| 715 | + } |
| 716 | + chart.data.labels = newLabels; |
| 717 | + chart.data.datasets = newDatasets; |
| 718 | + var newMaxIdx = Math.max(0, newLabels.length - 1); |
| 719 | + var zoomLimits = chart.options.plugins |
| 720 | + && chart.options.plugins.zoom |
| 721 | + && chart.options.plugins.zoom.limits |
| 722 | + && chart.options.plugins.zoom.limits.x; |
| 723 | + if (zoomLimits) { |
| 724 | + zoomLimits.max = newMaxIdx; |
| 725 | + } |
| 726 | + syncSliderBounds(card, newLabels.length); |
| 727 | + // Keep the user's "scope" (number of visible commits) but anchor the |
| 728 | + // window on the newest commit so they don't drift backwards in time |
| 729 | + // unexpectedly. Without this anchoring, the visible range would still |
| 730 | + // be `[0, oldN-1]` — i.e., the *oldest* `oldN` commits of the new |
| 731 | + // payload — which is the opposite of what the user wanted when they |
| 732 | + // zoomed out. |
| 733 | + var sx = chart.options.scales.x; |
| 734 | + var prevMin = Number.isFinite(sx.min) ? sx.min : 0; |
| 735 | + var prevMax = Number.isFinite(sx.max) ? sx.max : 0; |
| 736 | + var prevVisible = Math.max(1, prevMax - prevMin + 1); |
| 737 | + sx.max = newMaxIdx; |
| 738 | + sx.min = Math.max(0, newMaxIdx - (prevVisible - 1)); |
| 739 | + rebuildVisibleAndUpdate(card, chart, sx.min, sx.max); |
| 740 | + if (canvas.__bench_strip_render) canvas.__bench_strip_render(); |
620 | 741 | } |
621 | 742 |
|
622 | 743 | // Mirror the chart's current visible commit count onto the toolbar |
|
664 | 785 |
|
665 | 786 | // ----------------------------------------------------------------------- |
666 | 787 | // Per-card construction. State lives on the canvas: |
667 | | - // canvas.__bench_chart — Chart.js instance |
668 | | - // canvas.__bench_payload — last-fetched ChartResponse (raw) |
669 | | - // canvas.__bench_state — { y, scope } (per-chart toolbar state) |
| 788 | + // canvas.__bench_chart — Chart.js instance |
| 789 | + // canvas.__bench_payload — last-fetched ChartResponse (raw) |
| 790 | + // canvas.__bench_state — { y, scope } (per-chart toolbar state) |
| 791 | + // canvas.__bench_inline_trimmed — true if the payload came from an |
| 792 | + // inline `<script id="chart-data-N">` |
| 793 | + // and may have been capped at |
| 794 | + // LANDING_INLINE_N commits server-side |
| 795 | + // canvas.__bench_full_loaded — true once a `?n=all` refetch has |
| 796 | + // replaced the payload |
| 797 | + // canvas.__bench_full_fetch_pending — in-flight refetch flag (dedupe) |
670 | 798 | // ----------------------------------------------------------------------- |
671 | 799 | function constructChart(card) { |
672 | 800 | var idx = card.getAttribute("data-chart-index"); |
673 | 801 | var canvas = card.querySelector('canvas[data-chart-index="' + idx + '"]'); |
674 | 802 | if (!canvas || typeof Chart === "undefined") return null; |
675 | 803 | if (canvas.__bench_chart) return canvas.__bench_chart; |
676 | 804 |
|
| 805 | + var payloadFromInline = !canvas.__bench_payload; |
677 | 806 | var payload = canvas.__bench_payload || readInlinePayload(idx); |
678 | 807 | if (!payload) return null; |
679 | 808 | canvas.__bench_payload = payload; |
| 809 | + // Server caps inline payloads at LANDING_INLINE_N commits. Reaching that |
| 810 | + // count means there might be more on the server; if we got fewer, we |
| 811 | + // have the whole history already and never need to refetch. |
| 812 | + if (canvas.__bench_full_loaded === undefined) { |
| 813 | + var inlineN = (payload.commits || []).length; |
| 814 | + canvas.__bench_inline_trimmed = |
| 815 | + payloadFromInline && inlineN >= LANDING_INLINE_N; |
| 816 | + canvas.__bench_full_loaded = !canvas.__bench_inline_trimmed; |
| 817 | + } |
680 | 818 |
|
681 | 819 | var state = canvas.__bench_state || { y: "linear", scope: DEFAULT_VISIBLE }; |
682 | 820 | canvas.__bench_state = state; |
|
0 commit comments