bobframes 0.2.6__tar.gz → 0.2.7__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 (392) hide show
  1. {bobframes-0.2.6 → bobframes-0.2.7}/CHANGELOG.md +41 -1
  2. {bobframes-0.2.6 → bobframes-0.2.7}/PKG-INFO +1 -1
  3. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/_default_config.toml +5 -1
  4. bobframes-0.2.7/bobframes/_version.py +1 -0
  5. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/aggregates.py +66 -0
  6. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/config.py +10 -0
  7. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/package.py +27 -0
  8. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/components.js +13 -1
  9. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/dashboard.py +28 -20
  10. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/discovery.py +9 -0
  11. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/draws_by_class.py +16 -7
  12. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/instancing_opportunities.py +4 -3
  13. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/orchestrator.py +11 -0
  14. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/overdraw.py +3 -1
  15. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/pass_gpu.py +16 -6
  16. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/preview.py +4 -4
  17. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/shader_hotlist.py +1 -0
  18. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/summary.py +13 -8
  19. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/trend_table.py +84 -32
  20. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/draws_by_class.html +9 -2
  21. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/index.html +10 -3
  22. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/instancing_opportunities.html +9 -2
  23. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/overdraw.html +8 -1
  24. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/pass_gpu.html +11 -4
  25. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/draws_by_class.html +11 -5
  26. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/index.html +10 -3
  27. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/instancing_opportunities.html +9 -3
  28. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/overdraw.html +10 -4
  29. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/pass_gpu.html +9 -3
  30. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/shader_hotlist.html +9 -3
  31. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/summary.html +10 -4
  32. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/shader_hotlist.html +8 -1
  33. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/summary.html +10 -3
  34. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/trend_table.html +24 -17
  35. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/draws_by_class.html +9 -2
  36. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/index.html +10 -3
  37. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/instancing_opportunities.html +9 -2
  38. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/overdraw.html +8 -1
  39. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/pass_gpu.html +11 -4
  40. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/draws_by_class.html +11 -5
  41. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/index.html +10 -3
  42. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/instancing_opportunities.html +9 -3
  43. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/overdraw.html +10 -4
  44. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/pass_gpu.html +9 -3
  45. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/shader_hotlist.html +9 -3
  46. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/summary.html +10 -4
  47. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/shader_hotlist.html +8 -1
  48. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/summary.html +10 -3
  49. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/trend_table.html +24 -17
  50. {bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted → bobframes-0.2.7/bobframes/tests/data/golden_package/shared}/_assets/report.js +8 -1
  51. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/draws_by_class.html +1 -1
  52. bobframes-0.2.7/bobframes/tests/data/golden_package/shared/_reports/index.html +44 -0
  53. bobframes-0.2.7/bobframes/tests/data/golden_package/shared/_reports/instancing_opportunities.html +46 -0
  54. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/pass_gpu.html +3 -3
  55. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/draws_by_class.html +3 -4
  56. bobframes-0.2.7/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/index.html +45 -0
  57. bobframes-0.2.7/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/instancing_opportunities.html +43 -0
  58. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/overdraw.html +2 -3
  59. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/pass_gpu.html +1 -2
  60. bobframes-0.2.7/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/shader_hotlist.html +43 -0
  61. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/summary.html +2 -3
  62. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/summary.html +2 -2
  63. bobframes-0.2.7/bobframes/tests/data/golden_package/shared/_reports/trend_table.html +57 -0
  64. {bobframes-0.2.6/bobframes/tests/data/golden_package/shared → bobframes-0.2.7/bobframes/tests/data/golden_package/shared_redacted}/_assets/report.js +8 -1
  65. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/draws_by_class.html +1 -1
  66. bobframes-0.2.7/bobframes/tests/data/golden_package/shared_redacted/_reports/index.html +44 -0
  67. bobframes-0.2.7/bobframes/tests/data/golden_package/shared_redacted/_reports/instancing_opportunities.html +46 -0
  68. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/pass_gpu.html +3 -3
  69. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/draws_by_class.html +3 -4
  70. bobframes-0.2.7/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/index.html +45 -0
  71. bobframes-0.2.7/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/instancing_opportunities.html +43 -0
  72. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/overdraw.html +2 -3
  73. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/pass_gpu.html +1 -2
  74. bobframes-0.2.7/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/shader_hotlist.html +43 -0
  75. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/summary.html +2 -3
  76. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/summary.html +2 -2
  77. bobframes-0.2.7/bobframes/tests/data/golden_package/shared_redacted/_reports/trend_table.html +57 -0
  78. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_preview/_chrome_preview.html +9 -2
  79. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_aggregates.py +69 -2
  80. bobframes-0.2.7/bobframes/tests/test_browser_shots.py +90 -0
  81. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_config.py +6 -1
  82. bobframes-0.2.7/bobframes/tests/test_draws_by_class_kpis.py +34 -0
  83. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_package.py +31 -5
  84. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_report_polish.py +3 -2
  85. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_report_structure.py +41 -10
  86. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_run_model.py +32 -1
  87. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_summary.py +6 -6
  88. bobframes-0.2.7/bobframes/tests/test_trend_regression_basis.py +103 -0
  89. bobframes-0.2.6/bobframes/_version.py +0 -1
  90. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/index.html +0 -44
  91. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/instancing_opportunities.html +0 -46
  92. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/index.html +0 -45
  93. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/instancing_opportunities.html +0 -44
  94. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/shader_hotlist.html +0 -44
  95. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/trend_table.html +0 -57
  96. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/index.html +0 -44
  97. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/instancing_opportunities.html +0 -46
  98. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/index.html +0 -45
  99. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/instancing_opportunities.html +0 -44
  100. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/shader_hotlist.html +0 -44
  101. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/trend_table.html +0 -57
  102. bobframes-0.2.6/bobframes/tests/test_browser_shots.py +0 -49
  103. {bobframes-0.2.6 → bobframes-0.2.7}/.gitignore +0 -0
  104. {bobframes-0.2.6 → bobframes-0.2.7}/LICENSE +0 -0
  105. {bobframes-0.2.6 → bobframes-0.2.7}/README.md +0 -0
  106. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/__init__.py +0 -0
  107. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/catalog.py +0 -0
  108. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/cli.py +0 -0
  109. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derive_post_merge.py +0 -0
  110. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derives/__init__.py +0 -0
  111. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derives/classifier.py +0 -0
  112. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derives/draw_classifier.toml +0 -0
  113. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derives/pass_class_breakdown.py +0 -0
  114. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derives/presets/custom-template.toml +0 -0
  115. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derives/presets/godot.toml +0 -0
  116. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derives/presets/unity.toml +0 -0
  117. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/derives/texture_usage.py +0 -0
  118. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/discovery.py +0 -0
  119. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/errors.py +0 -0
  120. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/global_entities.py +0 -0
  121. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/health.py +0 -0
  122. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/html/__init__.py +0 -0
  123. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/html/template.py +0 -0
  124. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/lint.py +0 -0
  125. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/lint_banlist.toml +0 -0
  126. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/manifest.py +0 -0
  127. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/parquetize.py +0 -0
  128. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/parsers/__init__.py +0 -0
  129. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/parsers/derive_program_transitions.py +0 -0
  130. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/parsers/parse_init_state.py +0 -0
  131. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/paths.py +0 -0
  132. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/probes/__init__.py +0 -0
  133. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/probes/whatif.py +0 -0
  134. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/qrd_harness.py +0 -0
  135. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/query_examples.py +0 -0
  136. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/rdcmd.py +0 -0
  137. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/replay/__init__.py +0 -0
  138. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/replay/replay_main.py +0 -0
  139. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/__init__.py +0 -0
  140. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/_tokens.py +0 -0
  141. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/ab.py +0 -0
  142. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/Inter-OFL.txt +0 -0
  143. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/README.md +0 -0
  144. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/chrome.css +0 -0
  145. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/components.css +0 -0
  146. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/container.css +0 -0
  147. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/design_tokens.css +0 -0
  148. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/icon_sprite.html +0 -0
  149. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/inter-subset.woff2 +0 -0
  150. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/link_kind.css +0 -0
  151. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/per_drop.css +0 -0
  152. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/print.css +0 -0
  153. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/rdc_table.css +0 -0
  154. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/rdc_table.js +0 -0
  155. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/assets/sticky.css +0 -0
  156. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/base.py +0 -0
  157. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/cache.py +0 -0
  158. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/charts.py +0 -0
  159. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/chrome.py +0 -0
  160. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/cli.py +0 -0
  161. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/delta.py +0 -0
  162. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/design_tokens.toml +0 -0
  163. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/reports/formatters.py +0 -0
  164. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/resource_labels.py +0 -0
  165. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/run.py +0 -0
  166. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/schemas.py +0 -0
  167. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/stable_keys.py +0 -0
  168. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/__init__.py +0 -0
  169. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/_render_util.py +0 -0
  170. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_pagedata/catalog.js +0 -0
  171. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  172. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  173. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  174. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  175. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  176. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  177. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  178. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  179. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  180. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  181. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  182. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  183. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  184. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  185. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  186. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  187. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  188. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  189. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  190. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  191. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  192. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  193. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  194. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  195. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  196. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  197. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  198. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden/index.html +0 -0
  199. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/README.txt +0 -0
  200. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_pagedata/catalog.js +0 -0
  201. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  202. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  203. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  204. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  205. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  206. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  207. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  208. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  209. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  210. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  211. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  212. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  213. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  214. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  215. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  216. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  217. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  218. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  219. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  220. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  221. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  222. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  223. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  224. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  225. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  226. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  227. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  228. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/redacted/index.html +0 -0
  229. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/README.txt +0 -0
  230. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_assets/catalog.css +0 -0
  231. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_assets/catalog.js +0 -0
  232. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_assets/report.css +0 -0
  233. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_pagedata/catalog.js +0 -0
  234. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  235. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  236. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  237. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  238. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  239. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  240. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  241. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  242. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  243. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  244. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  245. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  246. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  247. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  248. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  249. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  250. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  251. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  252. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  253. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  254. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  255. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  256. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  257. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  258. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  259. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  260. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  261. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/overdraw.html +0 -0
  262. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/_reports/shader_hotlist.html +0 -0
  263. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared/index.html +0 -0
  264. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/README.txt +0 -0
  265. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_assets/catalog.css +0 -0
  266. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_assets/catalog.js +0 -0
  267. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_assets/report.css +0 -0
  268. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_pagedata/catalog.js +0 -0
  269. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  270. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  271. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  272. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  273. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  274. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  275. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  276. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  277. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  278. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  279. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  280. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  281. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  282. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  283. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  284. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  285. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  286. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  287. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  288. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  289. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  290. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  291. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  292. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  293. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  294. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  295. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  296. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/overdraw.html +0 -0
  297. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/_reports/shader_hotlist.html +0 -0
  298. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_package/shared_redacted/index.html +0 -0
  299. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/golden_parquet/digests.json +0 -0
  300. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +0 -0
  301. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
  302. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
  303. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
  304. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
  305. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
  306. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
  307. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
  308. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
  309. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
  310. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
  311. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
  312. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
  313. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
  314. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
  315. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
  316. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
  317. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
  318. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
  319. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
  320. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
  321. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
  322. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
  323. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
  324. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
  325. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
  326. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
  327. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
  328. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +0 -0
  329. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
  330. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
  331. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
  332. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
  333. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
  334. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
  335. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
  336. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
  337. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
  338. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
  339. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
  340. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
  341. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
  342. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
  343. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
  344. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
  345. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
  346. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
  347. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
  348. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
  349. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
  350. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
  351. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
  352. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
  353. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
  354. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
  355. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
  356. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/make_golden.py +0 -0
  357. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/make_package_golden.py +0 -0
  358. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/make_parquet_golden.py +0 -0
  359. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/make_preview_golden.py +0 -0
  360. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/make_synthetic.py +0 -0
  361. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/smoke.py +0 -0
  362. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_assets.py +0 -0
  363. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_cache.py +0 -0
  364. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_catalog_backup_dedup.py +0 -0
  365. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_charts.py +0 -0
  366. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_classifier.py +0 -0
  367. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_components.py +0 -0
  368. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_contrast.py +0 -0
  369. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_delta.py +0 -0
  370. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_design_tokens.py +0 -0
  371. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_determinism.py +0 -0
  372. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_discovery.py +0 -0
  373. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_element_builder.py +0 -0
  374. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_fonts.py +0 -0
  375. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_hardening.py +0 -0
  376. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_head_assets.py +0 -0
  377. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_health.py +0 -0
  378. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_js_coupled_classes.py +0 -0
  379. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_manifest_guard.py +0 -0
  380. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_multicapture_normalize.py +0 -0
  381. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_parity.py +0 -0
  382. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_parquet_parity.py +0 -0
  383. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_parquetize.py +0 -0
  384. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_perf.py +0 -0
  385. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_replay_drift.py +0 -0
  386. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_schemas.py +0 -0
  387. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_schemas_unit.py +0 -0
  388. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_stable_keys.py +0 -0
  389. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_table_component.py +0 -0
  390. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_theme.py +0 -0
  391. {bobframes-0.2.6 → bobframes-0.2.7}/bobframes/tests/test_token_guard.py +0 -0
  392. {bobframes-0.2.6 → bobframes-0.2.7}/pyproject.toml +0 -0
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.7] - 2026-06-24
11
+
12
+ An aggregation-consistency pass plus four report/sharing correctness fixes. Every KPI now names its
13
+ estimator and rate KPIs read on one per-frame basis, so the same metric agrees across the summary,
14
+ dashboard, and trend. No schema change (still schema 3); parquet digests stay byte-identical on the
15
+ same captures. `_version` 0.2.6 -> 0.2.7.
16
+
17
+ ### Changed
18
+ - One canonical aggregation policy (ADR-46): every rendered KPI label names its estimator precisely --
19
+ `Pooled mean`, `Mean (per area)`, `Median`, `Total` -- and never a bare "avg". Rate KPIs (GPU, draws)
20
+ read per-frame on a single basis, so the same area's GPU now reads identically across the summary,
21
+ dashboard, and trend (previously e.g. `0.0356`/frame on the summary vs a raw `0.178` total on the
22
+ dashboard with no bridge). A naming gate (`test_no_vague_estimator_labels`) fails the build on any
23
+ vague estimator word. (D-13..D-16, Q-10..Q-13)
24
+ - Regression detection unified on the per-frame basis with config-driven thresholds: `trend_table`
25
+ reads every KPI per frame, so a regression flag is capture-count-independent and agrees with the
26
+ build-health verdict (a 7-vs-5-capture run no longer reports a false +40%); the per-KPI thresholds
27
+ (`draws`/`vbo`/`ibo`/`program_switches`) move from code literals to the `[report]` config, with
28
+ defaults that reproduce the prior values. (H-41)
29
+ - `draws_by_class` ratios use `statistics.median` (was the upper-middle `sorted[n//2]`, an off-by-one
30
+ on even N).
31
+ - `aggregates.frame_counts` is the single owner of every per-(drop, area) frame count; when the
32
+ GPU/draws denominator and the entity-rate denominator legitimately differ (a capture that replayed
33
+ but exported partial entity rows), the divergence is logged as a WARNING rather than silently
34
+ normalized away.
35
+
36
+ ### Fixed
37
+ - Run-selector dropdown did nothing on every report: the component JS runs in `<head>`, so the custom
38
+ element upgraded before its child `<select>` had parsed; initialization now defers to
39
+ `DOMContentLoaded`. (R-20)
40
+ - Exported standalone one-pager carried dead tree-relative navigation -- a run dropdown pointing at
41
+ siblings not in the bundle, a breadcrumb, the dashboard link, and a "viewing an older run" banner.
42
+ The detached summary now strips them; the in-tree summary keeps its working navigation. (R-21)
43
+ - Older-run report pages showed cross-drop graphs and tables spanning all runs, including ones newer
44
+ than the page's own run. Cross-drop renderers now scope to run history (up to and including the
45
+ current run) while the run picker keeps the full list. (R-22)
46
+
10
47
  ## [0.2.6] - 2026-06-06
11
48
 
12
49
  A build-health one-pager, a shareable `package` bundle, a server-side component system, and a full visual
@@ -123,5 +160,8 @@ First standalone release. v1 is Windows-only (the replay stage drives `qrenderdo
123
160
  `python -m _analysis.*` entry points no longer work; switch to the `bobframes` commands (see the
124
161
  migration table in the README). This is a hard rename with no compatibility shim.
125
162
 
126
- [Unreleased]: https://github.com/altpsyche/bobframes/compare/v0.1.0...HEAD
163
+ [Unreleased]: https://github.com/altpsyche/bobframes/compare/v0.2.7...HEAD
164
+ [0.2.7]: https://github.com/altpsyche/bobframes/compare/v0.2.6...v0.2.7
165
+ [0.2.6]: https://github.com/altpsyche/bobframes/compare/v0.2.0...v0.2.6
166
+ [0.2.0]: https://github.com/altpsyche/bobframes/compare/v0.1.0...v0.2.0
127
167
  [0.1.0]: https://github.com/altpsyche/bobframes/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bobframes
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: RenderDoc capture pipeline: ingest, analyze, render.
5
5
  Project-URL: Homepage, https://github.com/altpsyche/bobframes
6
6
  Project-URL: Issues, https://github.com/altpsyche/bobframes/issues
@@ -57,5 +57,9 @@ shader_complexity_high = 60.0 # fragment shaders at/above this complexity a
57
57
  overdraw_reject_warn_pct = 40.0 # RT sample-rejection % -> warn callout
58
58
  overdraw_reject_alarm_pct = 70.0 # RT sample-rejection % -> alarm callout
59
59
  instancing_repeat_min = 4 # min mesh repeat PER FRAME to flag an instancing candidate (c16v: repeat + shader cost are per-frame rates, normalized by the drop's captured-frame count)
60
- gpu_regression_pct = 10.0 # latest-vs-previous GPU increase % -> regression callout
60
+ gpu_regression_pct = 10.0 # latest-vs-previous GPU/frame increase % -> regression (per frame, capture-count-independent)
61
+ draws_regression_pct = 10.0 # H-41 — per-KPI trend regression thresholds (were baked in trend_table.KPIS literals)
62
+ vbo_regression_pct = 15.0 # " vertex-buffer bytes / frame
63
+ ibo_regression_pct = 15.0 # " index-buffer bytes / frame
64
+ program_switches_regression_pct = 20.0 # " program switches / frame
61
65
  max_prerendered_runs = 10 # c16f — cap on pre-rendered older-run pages (run selector)
@@ -0,0 +1 @@
1
+ __version__ = "0.2.7"
@@ -163,3 +163,69 @@ def shader_aggregates(root: str, drops: list, *, stage: str | None = None) -> Sh
163
163
  sa.cplx[k] = max(sa.cplx.get(k, 0.0), c_val)
164
164
  sa.stype[k] = stype
165
165
  return sa
166
+
167
+
168
+ # --- frame counts (D-15: the single owner of every per-(drop, area) frame count) ----------------
169
+
170
+ def _frame_total_captures(root: str, drops: list) -> dict:
171
+ """``{(drop_key, area): {captures}}`` from ``frame_totals.parquet`` -- the authoritative
172
+ captured-frame set (the distinct ``capture`` values present in the per-frame GPU/draws table that
173
+ ``dashboard._run_totals`` / ``_top_areas_gpu`` divide by). Read live per drop_dir (frame_totals has
174
+ no per-drop cache, mirroring the dashboard readers). If a frame_totals has no ``capture`` column,
175
+ each row is treated as one distinct frame (num_rows) so the count never under-reports."""
176
+ out: dict = defaultdict(set)
177
+ for d in drops:
178
+ for r in d.rows:
179
+ p = os.path.join(r.drop_dir, 'frame_totals.parquet')
180
+ if not os.path.exists(p):
181
+ continue
182
+ try:
183
+ names = set(papq.read_schema(p).names)
184
+ if 'capture' in names:
185
+ caps = papq.read_table(p, columns=['capture']).column('capture').to_pylist()
186
+ else:
187
+ caps = [f'__row_{i}' for i in range(papq.read_metadata(p).num_rows)]
188
+ except Exception:
189
+ continue
190
+ out[(drop_key(r.drop_date, r.drop_label), r.area)].update(caps)
191
+ return out
192
+
193
+
194
+ def frame_counts(root: str, drops: list) -> dict:
195
+ """The ONE owner of every per-(drop, area) frame count (D-15 / D-A4). Returns
196
+ ``{(drop_key, area): {'frames', 'draw_frames', 'shader_frames'}}`` where ``frames`` = distinct
197
+ captures in frame_totals (the GPU/draws-per-frame denominator) and ``draw_frames``/``shader_frames``
198
+ = distinct captures in the draws/shaders entity data (the c16v entity-rate denominators).
199
+
200
+ The three legitimately DIFFER when a capture replayed "ok" but exported no entity rows (the c16v
201
+ as-built, ADR-23): per-frame GPU divides by the frames that produced GPU data, while per-frame mesh/
202
+ shader rates divide by the frames that produced that entity -- both correct for their metric, on
203
+ different populations. ``frame_count_divergences`` surfaces the difference so the different-N
204
+ normalization is visible across reports rather than silent. Raw counts (no >=1 guard) so divergence
205
+ is detectable; the per-frame no-op guard stays in ``DrawAgg.frames`` / ``ShaderAgg.frames``.
206
+ """
207
+ da = draw_aggregates(root, drops)
208
+ sa = shader_aggregates(root, drops)
209
+ ft = _frame_total_captures(root, drops)
210
+ keys = set(ft) | set(da.captures) | set(sa.captures)
211
+ return {
212
+ k: {
213
+ 'frames': len(ft.get(k, ())),
214
+ 'draw_frames': len(da.captures.get(k, ())),
215
+ 'shader_frames': len(sa.captures.get(k, ())),
216
+ }
217
+ for k in keys
218
+ }
219
+
220
+
221
+ def frame_count_divergences(root: str, drops: list) -> list:
222
+ """``[(drop_key, area, frames, draw_frames, shader_frames), ...]`` for every (drop, area) where an
223
+ entity count that EXISTS (>0) differs from the frame_totals frame count -- i.e. a capture produced
224
+ GPU/draw data but not the matching entity rows (or vice versa). Sorted for deterministic logging.
225
+ Empty when every layer agrees (the uniform-capture case). Drives the render-time warning (D-15)."""
226
+ out = []
227
+ for (dk, area), c in sorted(frame_counts(root, drops).items()):
228
+ ftn, dfr, sfr = c['frames'], c['draw_frames'], c['shader_frames']
229
+ if (dfr and dfr != ftn) or (sfr and sfr != ftn):
230
+ out.append((dk, area, ftn, dfr, sfr))
231
+ return out
@@ -131,6 +131,12 @@ class ReportCfg: # c16 — report-presentation thresholds (
131
131
  overdraw_reject_alarm_pct: float = 70.0
132
132
  instancing_repeat_min: int = 4 # c16v — min mesh repeat PER FRAME (repeat + shader cost are per-frame rates)
133
133
  gpu_regression_pct: float = 10.0
134
+ # H-41 (v027_1) — per-KPI trend regression thresholds; were baked in `trend_table.KPIS` literals.
135
+ # Trend-only (health's verdict has only a gpu trajectory rule). Defaults reproduce the old literals.
136
+ draws_regression_pct: float = 10.0
137
+ vbo_regression_pct: float = 15.0
138
+ ibo_regression_pct: float = 15.0
139
+ program_switches_regression_pct: float = 20.0
134
140
  max_prerendered_runs: int = 10 # c16f — cap on pre-rendered older-run pages (per-run UX)
135
141
 
136
142
 
@@ -327,6 +333,10 @@ def _build_config(root: str | None) -> Config:
327
333
  overdraw_reject_alarm_pct=rpt.get('overdraw_reject_alarm_pct', 70.0),
328
334
  instancing_repeat_min=rpt.get('instancing_repeat_min', 4),
329
335
  gpu_regression_pct=rpt.get('gpu_regression_pct', 10.0),
336
+ draws_regression_pct=rpt.get('draws_regression_pct', 10.0),
337
+ vbo_regression_pct=rpt.get('vbo_regression_pct', 15.0),
338
+ ibo_regression_pct=rpt.get('ibo_regression_pct', 15.0),
339
+ program_switches_regression_pct=rpt.get('program_switches_regression_pct', 20.0),
330
340
  max_prerendered_runs=rpt.get('max_prerendered_runs', 10),
331
341
  ),
332
342
  theme=ThemeCfg(tokens=tuple(theme_tokens.items())) if theme_tokens else None,
@@ -217,6 +217,9 @@ def build(root: str, *, out: str | None = None, light: bool = False,
217
217
  shutil.rmtree(staging, ignore_errors=True)
218
218
 
219
219
  if summary_file:
220
+ # The standalone one-pager is a DETACHED single file -- strip the tree-relative navigation that
221
+ # cannot resolve beside a lone HTML (R-21), so it no longer carries a dead run dropdown.
222
+ summary_bytes = _detach_summary(summary_bytes)
220
223
  with open(summary_path, 'wb') as f:
221
224
  f.write(summary_bytes)
222
225
  else:
@@ -503,6 +506,30 @@ def _redact_text_files(entries: list[tuple[str, str]], *, mode: str) -> int:
503
506
  return n
504
507
 
505
508
 
509
+ # Tree-relative navigation that only resolves inside the rendered tree -- dead in a detached one-pager.
510
+ # Each is a cleanly-delimited, non-nesting element generated by the chrome builders.
511
+ _DETACH_RES = (
512
+ # the run selector: its options point to run/<key>/summary.html siblings not bundled with a lone file
513
+ re.compile(rb'<rdc-ab-picker><label for="rdc-run-select">.*?</rdc-ab-picker>', re.S),
514
+ re.compile(rb'<nav class="crumb">.*?</nav>', re.S), # breadcrumb (root catalog / dashboard)
515
+ re.compile(rb'<a class="sb-link"[^>]*>.*?</a>', re.S), # summary-bar "dashboard" link
516
+ # the "viewing an older run / go to newest" nav banner (older-run one-pagers); its link points to a
517
+ # sibling page absent beside a lone file. Anchored on its title so the health alarm-banner is kept.
518
+ re.compile(rb'<rdc-alarm-banner\b[^>]*>(?:(?!</rdc-alarm-banner>).)*?viewing an older run'
519
+ rb'(?:(?!</rdc-alarm-banner>).)*?</rdc-alarm-banner>', re.S),
520
+ )
521
+
522
+
523
+ def _detach_summary(html: bytes) -> bytes:
524
+ """Make the standalone one-pager genuinely self-contained (R-21): strip the tree-relative navigation
525
+ that cannot work in a LONE file -- the run selector (a silently-dead dropdown), the breadcrumb, and
526
+ the summary-bar dashboard link. The page's own content (verdict, KPIs, movement, by-area) is
527
+ self-contained and untouched; the in-tree `_reports/summary.html` keeps its working nav."""
528
+ for rx in _DETACH_RES:
529
+ html = rx.sub(b'', html)
530
+ return html
531
+
532
+
506
533
  def _redacted_summary_bytes(staging: str, summary_rel: str, *, newest: bool, target,
507
534
  build_ts: str, shared: bool, redact_paths: str) -> bytes:
508
535
  """The standalone one-pager for a redacted bundle: self-contained (INLINE) + redacted.
@@ -6,7 +6,19 @@
6
6
  connectedCallback(){
7
7
  if (this._rdcUp) return;
8
8
  this._rdcUp = true;
9
- try { this.init(); } catch(e) { console.error('rdc init error', this.tagName, e); }
9
+ const run = () => {
10
+ try { this.init(); } catch(e) { console.error('rdc init error', this.tagName, e); }
11
+ };
12
+ // The component JS rides in <head> (adjacent to the CSS), so a custom element upgrades as its
13
+ // START tag is parsed -- BEFORE its light-DOM children (e.g. the run <select>) exist. Inits that
14
+ // querySelector a child would see nothing and bail. Defer to DOMContentLoaded while the document
15
+ // is still parsing so children are present; run immediately for elements created after load.
16
+ // (Mirrors the _wireRowDrill deferral below.)
17
+ if (document.readyState === 'loading'){
18
+ document.addEventListener('DOMContentLoaded', run, { once: true });
19
+ } else {
20
+ run();
21
+ }
10
22
  }
11
23
  init(){}
12
24
  }
@@ -301,11 +301,11 @@ def _global_kpis(drops: list) -> list:
301
301
  avg_gpu_frame = (total_gpu / n_frames) if n_frames else 0.0
302
302
  avg_draws_frame = (total_draws / n_frames) if n_frames else 0.0
303
303
  return [
304
- {'label': 'total gpu (s)', 'value': base.fmt_float(total_gpu, 3)},
305
- {'label': 'avg gpu / frame (s)', 'value': base.fmt_float(avg_gpu_frame, 4)},
306
- {'label': 'total draws', 'value': base.fmt_int(total_draws)},
307
- {'label': 'avg draws / frame', 'value': base.fmt_int(round(avg_draws_frame))},
308
- {'label': 'areas', 'value': base.fmt_int(len(areas))},
304
+ {'label': 'pooled mean gpu / frame (s)', 'value': base.fmt_float(avg_gpu_frame, 4)},
305
+ {'label': 'total gpu (s) over captures', 'value': base.fmt_float(total_gpu, 3)},
306
+ {'label': 'pooled mean draws / frame', 'value': base.fmt_int(round(avg_draws_frame))},
307
+ {'label': 'total draws over captures', 'value': base.fmt_int(total_draws)},
308
+ {'label': 'areas', 'value': base.fmt_int(len(areas))},
309
309
  ]
310
310
 
311
311
 
@@ -351,6 +351,9 @@ def build(root: str, *, drops: list | None = None, ab=None,
351
351
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
352
352
  cur = rc.current
353
353
  cur_drops = [cur] if cur else []
354
+ # v027_4: per-area captured-frame counts (the single owner, D-15) so the mini cards can read
355
+ # per-frame and stay consistent with summary + the trend card (no bare cross-capture totals).
356
+ area_frames = {a: c['frames'] for (_dk, a), c in _agg.frame_counts(root, cur_drops).items()}
354
357
 
355
358
  # c16f: a per-run dashboard lives at _reports/run/<key>/index.html; its sibling reports (instancing
356
359
  # etc.) are pre-rendered there too, so their links stay bare. trend_table is NOT per-run (it is the
@@ -398,22 +401,23 @@ def build(root: str, *, drops: list | None = None, ab=None,
398
401
  *[base.el('a', {'href': href, 'data-link-kind': 'primary'}, lbl)
399
402
  for href, lbl in _NAV]))
400
403
 
401
- # Card: trend table - GPU time per area (mini bars matching the trend flagship).
404
+ # Card: trend table - GPU PER FRAME per area (D-16: leads with the per-frame mean so the same area
405
+ # reads the SAME number here and on summary; the raw run total rides alongside as the labeled bridge).
402
406
  top_a = top_a[:3]
403
407
  chart_tt = base.figure(base.bar_chart(
404
- [(a, g) for a, g, *_ in top_a], value_fmt=lambda v: f'{v:.3f}', width=280,
405
- title='gpu (s) per area', desc='top areas by GPU seconds', chart_id='dash-tt'))
408
+ [(t[0], t[4]) for t in top_a], value_fmt=lambda v: f'{v:.4f}', width=280,
409
+ title='gpu / frame per area', desc='top areas by GPU per captured frame', chart_id='dash-tt'))
406
410
  body_tt = _card_table(
407
411
  top_a,
408
412
  [
409
413
  ('area', lambda r: r[0], False),
410
- ('gpu (s)', lambda r: base.fmt_float(r[1], 3), True),
411
- ('draws', lambda r: base.fmt_int(r[2]), True),
412
- ('avg draws / frame', lambda r: base.fmt_int(round(r[3])), True),
414
+ ('mean gpu / frame', lambda r: base.fmt_float(r[4], 4), True),
415
+ ('total gpu (s)', lambda r: base.fmt_float(r[1], 3), True),
416
+ ('mean draws / frame', lambda r: base.fmt_int(round(r[3])), True),
413
417
  ],
414
- caption='per-area GPU + draw load (avg draws per captured frame)')
415
- sub_tt = ('GPU time per area in the current run.'
416
- + (f' worst: {top_a[0][0]} {base.fmt_float(top_a[0][1], 3)}s' if top_a else ''))
418
+ caption='per-area GPU per captured frame + the run total (mean = area total / area frames)')
419
+ sub_tt = ('GPU per frame per area in the current run.'
420
+ + (f' worst: {top_a[0][0]} {base.fmt_float(top_a[0][4], 4)}s / frame' if top_a else ''))
417
421
  cards.append(_card(tt_href, 'trend table', sub_tt, chart_tt, body_tt))
418
422
 
419
423
  # Card: instancing - repeat per mesh (mini bars).
@@ -426,7 +430,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
426
430
  [
427
431
  ('mesh', lambda r: r[0], False),
428
432
  ('repeat', lambda r: base.fmt_int(r[1]), True),
429
- ('indices typ', lambda r: base.fmt_int(r[2]), True),
433
+ ('median verts', lambda r: base.fmt_int(r[2]), True),
430
434
  ],
431
435
  caption='most-repeated meshes')
432
436
  sub_im = ('Repeated meshes worth instancing or batching.'
@@ -438,7 +442,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
438
442
  top_p = _top_passes(cur_drops)
439
443
  chart_pg = base.figure(base.bar_chart(
440
444
  [(pl, g) for _, pl, g in top_p], value_fmt=lambda v: f'{v:.3f}', width=280,
441
- title='gpu (s) per pass', desc='heaviest passes by GPU seconds', chart_id='dash-pg'))
445
+ title='total gpu (s) per pass', desc='heaviest passes by total GPU seconds', chart_id='dash-pg'))
442
446
  body_pg = _card_table(
443
447
  top_p,
444
448
  [
@@ -447,9 +451,10 @@ def build(root: str, *, drops: list | None = None, ab=None,
447
451
  # so hover could never reveal it). The CSS clip now truncates the display + the td title=
448
452
  # reveals the full value on hover - consistent with every other mini text column.
449
453
  ('marker', lambda r: base.scrub_chrome_text(r[1]), False),
450
- ('gpu (s)', lambda r: base.fmt_float(r[2], 3), True),
454
+ # per-pass GPU is a cross-capture total (the pass report's native basis); labeled as such.
455
+ ('total gpu (s)', lambda r: base.fmt_float(r[2], 3), True),
451
456
  ],
452
- caption='heaviest passes by GPU time')
457
+ caption='heaviest passes by total GPU time')
453
458
  sub_pg = ('Heaviest GPU passes.'
454
459
  + (f' top: {top_p[0][1]} {base.fmt_float(top_p[0][2], 3)}s in {top_p[0][0]}'
455
460
  if top_p else ''))
@@ -503,10 +508,13 @@ def build(root: str, *, drops: list | None = None, ab=None,
503
508
  pa_rows,
504
509
  [
505
510
  ('area', lambda r: r[0], False),
506
- ('draws', lambda r: base.fmt_int(r[1]['n_draws']), True),
511
+ # per-frame to match summary's By-area + the trend card (D-16 consistency), not a bare total.
512
+ ('mean draws / frame',
513
+ lambda r: base.fmt_int(round(base.per_frame(r[1]['n_draws'], area_frames.get(r[0], 0)))),
514
+ True),
507
515
  ('dominant', lambda r: r[1]['dominant_class'], False),
508
516
  ],
509
- caption='top areas by draw count')
517
+ caption='top areas by mean draws per captured frame')
510
518
  dom = class_totals.most_common(1)[0] if class_totals else None
511
519
  sub_dc = ('Draw mix by class.'
512
520
  + (f' dominant: {dom[0]} {base.fmt_pct(100.0 * dom[1] / grand)}'
@@ -182,6 +182,15 @@ class RunContext:
182
182
  def is_newest(self) -> bool:
183
183
  return bool(self.current) and self.index == self.n_runs - 1
184
184
 
185
+ @property
186
+ def history(self) -> list:
187
+ """Drops up to AND INCLUDING the current run (chronological) -- the data scope for cross-drop
188
+ CHARTS/TABLES on a per-run page, so an OLDER run's page never shows data for runs that came
189
+ AFTER it (ADR-35; R-22). The run picker still lists every run (from `drops`); only the rendered
190
+ data is scoped. Equals `drops` on the newest page (index == n_runs - 1)."""
191
+ i = self.index
192
+ return list(self.drops) if i < 0 else self.drops[:i + 1]
193
+
185
194
  @property
186
195
  def run_label(self) -> str:
187
196
  """current.key (e.g. '2026-06-01_r110788'); '' when there is no current run."""
@@ -7,12 +7,14 @@ Segments colored by draw_class. Below: raw-count table.
7
7
  from __future__ import annotations
8
8
 
9
9
  import os
10
+ import statistics
10
11
  import sys
11
12
  from collections import Counter, defaultdict
12
13
 
13
14
  import pyarrow.parquet as papq
14
15
 
15
16
  from . import base
17
+ from .. import aggregates as _agg
16
18
 
17
19
 
18
20
  def _gather_from_drops(drops: list) -> tuple[dict, list, list, int]:
@@ -79,8 +81,11 @@ def _build_table(counts: dict, drop_keys: list) -> str:
79
81
  caption='raw draw counts per class, per area and drop')
80
82
 
81
83
 
82
- def _compute_kpis(counts: dict, areas: list) -> list:
83
- """Hero KPIs: total draws, n areas, median prepass/opaque ratio, dominant class."""
84
+ def _compute_kpis(counts: dict, areas: list, total_frames: int) -> list:
85
+ """Hero KPIs (current run): mean draws / frame + the labeled raw total, n areas, the MEDIAN
86
+ prepass/opaque ratio across areas, dominant class. Q-12: lead with the per-frame mean (the
87
+ capture-count-independent number) and label the total. Q-11: `statistics.median` (not the
88
+ upper-middle `sorted[n//2]`), and the label names the estimator + population."""
84
89
  total = sum(sum(cc.values()) for cc in counts.values())
85
90
  ratios = []
86
91
  class_totals: Counter = Counter()
@@ -90,14 +95,15 @@ def _compute_kpis(counts: dict, areas: list) -> list:
90
95
  op = cc.get('opaque', 0)
91
96
  if op:
92
97
  ratios.append(cc.get('prepass', 0) / op)
93
- median_ratio = (sorted(ratios)[len(ratios) // 2]
94
- if ratios else 0.0)
98
+ median_ratio = statistics.median(ratios) if ratios else 0.0
95
99
  dominant_cls = (class_totals.most_common(1)[0][0]
96
100
  if class_totals else '')
101
+ mean_draws = base.per_frame(total, total_frames) # per frame (no-op when frames<=1)
97
102
  return [
98
- {'label': 'total draws', 'value': base.fmt_int(total)},
103
+ {'label': 'mean draws / frame', 'value': base.fmt_int(round(mean_draws))},
104
+ {'label': 'total draws over captures', 'value': base.fmt_int(total)},
99
105
  {'label': 'areas', 'value': base.fmt_int(len(areas))},
100
- {'label': 'prepass/opaque (med)', 'value': base.fmt_float(median_ratio, 2)},
106
+ {'label': 'median prepass / opaque (across areas)', 'value': base.fmt_float(median_ratio, 2)},
101
107
  {'label': 'dominant class', 'value': dominant_cls or '-'},
102
108
  ]
103
109
 
@@ -112,9 +118,12 @@ def build(root: str, *, drops: list | None = None, ab=None,
112
118
  # table below keeps every run as the breakdown / comparison view.
113
119
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
114
120
  cur = rc.current
121
+ drops = rc.history # R-22: cross-drop data scoped to <= current (picker still lists all via rc)
115
122
  counts, areas, drop_keys, total_captures = _gather_from_drops(drops)
116
123
  cur_counts = {k: cc for k, cc in counts.items() if cur and k[1] == cur.key}
117
124
  cur_areas = cur.areas if cur else []
125
+ # Q-12: per-frame mean draws needs the current run's captured-frame count (the single owner, D-15).
126
+ cur_frames = sum(v['frames'] for v in (_agg.frame_counts(root, [cur]) if cur else {}).values())
118
127
 
119
128
  parts = []
120
129
 
@@ -183,7 +192,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
183
192
  drops=len(drops), captures=total_captures, build_ts=build_ts or base.now_iso(),
184
193
  crumb_depth=base.crumb_depth(ab, run=rc), ab=ab, root=root, report_key='draws_by_class',
185
194
  sink=sink, theme=theme,
186
- kpis=_compute_kpis(cur_counts, cur_areas), run=rc,
195
+ kpis=_compute_kpis(cur_counts, cur_areas, cur_frames), run=rc,
187
196
  device=base.provenance_strip(*base.newest_drop_provenance(root, [cur] if cur else []), redact=redact))])
188
197
 
189
198
 
@@ -71,6 +71,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
71
71
  # Run model (ADR-35): live candidates are meshes drawn in the CURRENT run; prior runs supply the
72
72
  # per-drop comparison columns + the resolved-since section, never inflate the live list.
73
73
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
74
+ drops = rc.history # R-22: cross-drop trend scoped to <= current (picker still lists all via rc)
74
75
  cur = rc.current
75
76
  bl = rc.baseline
76
77
  ck = cur.key if cur else None
@@ -220,7 +221,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
220
221
  if chart_items:
221
222
  mbody.append(base.figure(
222
223
  base.bar_chart(chart_items, title='estimated wasted indices',
223
- desc='(max repeat - 1) x typical indices, per mesh',
224
+ desc='(max repeat - 1) x median indices, per mesh',
224
225
  chart_id='inst-wasted'),
225
226
  'estimated wasted indices (top meshes)'))
226
227
 
@@ -237,9 +238,9 @@ def build(root: str, *, drops: list | None = None, ab=None,
237
238
  cols += [
238
239
  base.Column('areas', 'areas', clip='default'),
239
240
  base.Column('dompass', 'dominant pass', clip='narrow'),
240
- base.Column('ityp', 'indices typical', numeric=True),
241
+ base.Column('ityp', 'median indices', numeric=True),
241
242
  base.Column('wasted', 'wasted indices', numeric=True,
242
- title='(max repeat - 1) x typical indices: index data re-submitted across repeats',
243
+ title='(max repeat - 1) x median indices: index data re-submitted across repeats',
243
244
  render=lambda value, row: base.heatmap_cell(value, 0, row['_maxw'],
244
245
  text=base.fmt_int(value))),
245
246
  ]
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import time
6
6
 
7
+ from .. import aggregates as _agg
7
8
  from .. import lint
8
9
  from ..config import get_config
9
10
  from ..html import template
@@ -43,6 +44,16 @@ def render_all_reports(root: str, log, *,
43
44
  cache_out = reports_base.build_per_drop_cache(root)
44
45
  log(f' built per-drop cache: {cache_out} ({time.monotonic()-t0:.1f}s)')
45
46
 
47
+ # D-15 (D-A4): one owner of every per-(drop, area) frame count. Per-frame GPU/draws divide by the
48
+ # frame_totals frame count; per-frame mesh/shader rates divide by the entity-capture count (c16v).
49
+ # They legitimately differ when a capture replayed "ok" but exported no entity rows -- surface that
50
+ # so cross-report per-frame normalization uses different N visibly, not silently (ADR-23, warn not
51
+ # assert: the divergence is a real data condition, present even in the synthetic fixture).
52
+ for dk, area, ftn, dfr, sfr in _agg.frame_count_divergences(root, reports_base.discover_drops(root)):
53
+ log(f' WARNING: frame-count divergence {area} {dk}: frame_totals={ftn} draws={dfr} '
54
+ f'shaders={sfr} -- per-frame GPU/draws and per-frame entity rates use different '
55
+ f'denominators (a capture replayed but exported partial entity data)')
56
+
46
57
  for mod in all_reports():
47
58
  try:
48
59
  rep = mod.build(root, sink=sink, build_ts=build_ts, redact=redact, theme=theme)
@@ -133,8 +133,10 @@ def build(root: str, *, drops: list | None = None, ab=None,
133
133
  if drops is None:
134
134
  drops = base.discover_drops(root)
135
135
  # Run model (ADR-35): live render targets = those sampled in the CURRENT run; per-drop sample
136
- # columns below keep every run. A representative bucket is the current run's, not the oldest.
136
+ # columns below span the runs UP TO the current one (R-22: never runs that came after it; the run
137
+ # picker still lists every run). A representative bucket is the current run's, not the oldest.
137
138
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
139
+ drops = rc.history # R-22: scope cross-drop data to <= current
138
140
  cur = rc.current
139
141
  ck = cur.key if cur else None
140
142
  out_path = base.output_path(root, 'overdraw', ab, run=rc)
@@ -13,6 +13,7 @@ from collections import defaultdict
13
13
  import pyarrow.parquet as papq
14
14
 
15
15
  from . import base
16
+ from .. import aggregates as _agg
16
17
 
17
18
 
18
19
  def _aggregate(drops: list, ok_caps: set) -> dict:
@@ -68,9 +69,11 @@ def build(root: str, *, drops: list | None = None, ab=None,
68
69
  if drops is None:
69
70
  drops = base.discover_drops(root)
70
71
  # Run model (ADR-35): GPU headline + treemap + per-area ranking reflect the CURRENT run; the
71
- # per-drop GPU comparison rows below keep every run.
72
+ # per-drop GPU comparison rows below span the runs UP TO the current one (R-22: an older run's page
73
+ # must not show runs that came after it; the run picker still lists every run).
72
74
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
73
75
  cur = rc.current
76
+ drops = rc.history # R-22: scope cross-drop data to <= current
74
77
  ck = cur.key if cur else None
75
78
  out_path = base.output_path(root, 'pass_gpu', ab, run=rc)
76
79
  out_dir = os.path.dirname(out_path)
@@ -96,10 +99,17 @@ def build(root: str, *, drops: list | None = None, ab=None,
96
99
  total_gpu += g
97
100
  n_passes += 1
98
101
  cur_areas_set.add(area)
102
+ # Q-10: lead with GPU PER FRAME (pooled over the run's captured frames, the summary/dashboard
103
+ # basis) so this hero shares a magnitude with the rest of the reports; the raw cross-capture total
104
+ # rides alongside as the labeled "total ... over captures".
105
+ fc = _agg.frame_counts(root, [cur]) if cur else {}
106
+ total_frames = sum(v['frames'] for v in fc.values())
107
+ gpu_per_frame = (total_gpu / total_frames) if total_frames else 0.0
99
108
  kpis = [
100
- {'label': 'total gpu (s)', 'value': base.fmt_float(total_gpu, 3)},
101
- {'label': 'areas', 'value': base.fmt_int(len(cur_areas_set))},
102
- {'label': 'passes', 'value': base.fmt_int(n_passes)},
109
+ {'label': 'gpu / frame (s)', 'value': base.fmt_float(gpu_per_frame, 4)},
110
+ {'label': 'total gpu (s) over captures', 'value': base.fmt_float(total_gpu, 3)},
111
+ {'label': 'areas', 'value': base.fmt_int(len(cur_areas_set))},
112
+ {'label': 'passes', 'value': base.fmt_int(n_passes)},
103
113
  ]
104
114
 
105
115
  # Summary bar: top pass in the current run + area count
@@ -126,7 +136,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
126
136
  parts.append(base.callout(
127
137
  'info',
128
138
  f'heaviest pass: {area} / {marker_short}',
129
- f'{base.fmt_float(global_top_gpu, 3)}s GPU on the costliest capture - '
139
+ f'{base.fmt_float(global_top_gpu, 3)}s GPU summed over the run\'s captures - '
130
140
  f'profile this pass first.',
131
141
  href=f'#{base.h(area)}', link_text='jump to area'))
132
142
 
@@ -183,7 +193,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
183
193
 
184
194
  if len(drop_keys) >= 2:
185
195
  cells = ['<div class="bar-row">',
186
- '<span class="key" style="color: var(--text-2)">drops</span>',
196
+ '<span class="key" style="color: var(--text-2)">drops (total / drop)</span>',
187
197
  '<div style="display:flex;gap:var(--sp-3);font:var(--fs-small) ui-monospace,monospace;align-items:center;color:var(--text-2)">']
188
198
  prev_gpu = None
189
199
  for i, k in enumerate(drop_keys):
@@ -119,14 +119,14 @@ def _links_block() -> str:
119
119
 
120
120
  def _kpi_cards_block() -> str:
121
121
  cards = [
122
- base.kpi_card('avg draws / frame', base.fmt_int(28410),
122
+ base.kpi_card('pooled mean draws / frame', base.fmt_int(28410),
123
123
  delta_html='<span class="delta-pill neg">+4%</span>',
124
124
  trend=base.trendline([3, 5, 4, 8, 6, 9, 7], tone='neg'),
125
- note='5 areas - 142,050 total', tone='neg'),
126
- base.kpi_card('avg gpu / frame', base.fmt_float(12.8, 4),
125
+ note='pooled across 5 frames, 5 areas - 142,050 total', tone='neg'),
126
+ base.kpi_card('pooled mean gpu / frame', base.fmt_float(12.8, 4),
127
127
  delta_html='<span class="delta-pill pos">-1%</span>',
128
128
  trend=base.trendline([9, 7, 8, 6, 6, 5, 5], tone='pos'),
129
- note='0.064 s total', tone='pos'),
129
+ note='pooled across 5 frames - 0.064 s total', tone='pos'),
130
130
  ]
131
131
  return ('<div data-page-kind="summary"><div class="kpi-strip">'
132
132
  + ''.join(cards) + '</div></div>')
@@ -71,6 +71,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
71
71
  # Run model (ADR-35): the hotlist ranks the CURRENT run's shaders; prior runs feed the per-drop
72
72
  # comparison columns + the resolved-since section, never the cost ranking.
73
73
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
74
+ drops = rc.history # R-22: cross-drop comparison scoped to <= current (picker still lists all via rc)
74
75
  cur = rc.current
75
76
  bl = rc.baseline
76
77
  ck = cur.key if cur else None