collect_first_presentation.sh RawHistoryFolder

1#!/usr/bin/env bash
2# Collect exactly five isolated CEF/DMABUF cold starts. The application is
3# deliberately terminated after the bounded observation window; a completed
4# first-presentation summary, rather than process exit, is the success signal.
5set -euo pipefail
6
7readonly runs=5
8readonly timeout_seconds="${DUMBER_FIRST_PRESENTATION_TIMEOUT_SECONDS:-45}"
9readonly binary="${DUMBER_FIRST_PRESENTATION_BIN:-$PWD/dist/dumber}"
10readonly manifest="${DUMBER_BUILD_MANIFEST:-${binary}.manifest.json}"
11
12fail_unsafe_output() {
13  echo "first-presentation: unsafe output path: $1" >&2
14  exit 2
15}
16
17# The measured runtime is always explicit. Never default to a hardcoded path
18# or version: collection fails closed when the caller does not select one.
19if [[ -z "${DUMBER_CEF_DIR:-}" ]]; then
20  echo "first-presentation: DUMBER_CEF_DIR must be set to the selected CEF runtime directory" >&2
21  exit 2
22fi
23readonly runtime="${DUMBER_CEF_DIR}"
24# Never inherit a conflicting CEF_DIR override: the child environment below is
25# built with env -i and receives exactly this selected directory.
26readonly selected_cef_dir="${DUMBER_CEF_DIR}"
27
28if [[ -v DUMBER_FIRST_PRESENTATION_OUTPUT ]]; then
29  output="$DUMBER_FIRST_PRESENTATION_OUTPUT"
30else
31  # Evidence is state, not source: keep the default outside the checkout and
32  # make every invocation a fresh child of the external evidence directory.
33  readonly state_home="${XDG_STATE_HOME:-$HOME/.local/state}"
34  [[ "$state_home" == /* ]] || fail_unsafe_output "XDG_STATE_HOME must be absolute"
35  readonly evidence_root="$state_home/dumber/roadmap-evidence"
36  mkdir -p -- "$evidence_root" || fail_unsafe_output "could not create XDG state directory"
37  canonical_evidence_root="$(realpath -e -- "$evidence_root")" || \
38    fail_unsafe_output "XDG state directory cannot be canonicalized"
39  [[ "$canonical_evidence_root" == "$evidence_root" && ! -L "$evidence_root" ]] || \
40    fail_unsafe_output "XDG state directory must not contain symbolic links"
41  output="$evidence_root/first-presentation-$(date -u +%Y%m%dT%H%M%S)-$$"
42fi
43readonly output
44readonly upstream_module="github.com/bnema/purego-cef2gtk"
45readonly script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
46readonly runtime_probe="$script_dir/cef_runtime_probe.py"
47
48# Resolve provenance from the measured binary, not the collection checkout.
49# Reads embedded build info via `go version -m` and requires a build manifest
50# tied to the candidate SHA-256 when VCS data is absent (including
51# buildvcs=false builds). Never queries the checkout module graph.
52resolve_binary_provenance() {
53  python3 - "$binary" "$manifest" "$upstream_module" <<'PY'
54import hashlib, json, os, re, subprocess, sys
55binary, manifest_path, module = sys.argv[1:]
56
57def fail():
58    # Do not expose cache locations or other machine-local values.
59    raise SystemExit("first-presentation: immutable module provenance is unavailable")
60
61with open(binary, "rb") as candidate:
62    # Chunked streaming hash: hashlib.file_digest needs 3.11+, this stays
63    # compatible with older 3.x interpreters.
64    digest = hashlib.sha256()
65    for chunk in iter(lambda: candidate.read(65536), b""):
66        digest.update(chunk)
67    binary_sha256 = digest.hexdigest()
68
69try:
70    buildinfo = subprocess.check_output(["go", "version", "-m", binary], text=True, stderr=subprocess.DEVNULL)
71except (OSError, subprocess.CalledProcessError):
72    raise SystemExit("first-presentation: immutable module provenance is unavailable")
73if "=>" in buildinfo:
74    raise SystemExit("first-presentation: immutable module provenance is unavailable")
75
76mod_version = None
77dep_version = None
78vcs_revision = None
79for line in buildinfo.splitlines():
80    parts = line.split()
81    if len(parts) < 2:
82        continue
83    if parts[0] == "mod" and len(parts) >= 3 and parts[1] == "github.com/bnema/dumber":
84        mod_version = parts[2]
85    if parts[0] == "dep" and len(parts) >= 3 and parts[1] == module:
86        dep_version = parts[2]
87    if parts[0] == "build" and len(parts) >= 2 and parts[1].startswith("vcs.revision="):
88        vcs_revision = parts[1].split("=", 1)[1]
89if dep_version is None:
90    fail()
91pseudo_match = re.fullmatch(r"(v\d+\.\d+\.\d+)-0\.\d{14}-([0-9a-f]{12})", dep_version)
92tag_match = re.fullmatch(r"v\d+\.\d+\.\d+", dep_version)
93if not pseudo_match and not tag_match:
94    fail()
95if vcs_revision is not None and not re.fullmatch(r"[0-9a-f]{40}", vcs_revision):
96    fail()
97
98try:
99    with open(manifest_path, encoding="utf-8") as manifest_file:
100        manifest = json.load(manifest_file)
101except (OSError, ValueError):
102    manifest = None
103
104if manifest is None:
105    # Without embedded VCS data the binary cannot be attributed; fail closed.
106    if vcs_revision is None:
107        raise SystemExit("first-presentation: build manifest is required for binaries without embedded VCS data")
108    # Embedded VCS revision attributes the binary itself, but the full
109    # upstream revision is not in build info; the manifest must still bind it.
110    raise SystemExit("first-presentation: build manifest is required to bind dependency revisions")
111
112if not isinstance(manifest, dict):
113    raise SystemExit("first-presentation: build manifest is required for binaries without embedded VCS data")
114if manifest.get("binary_sha256") != binary_sha256:
115    raise SystemExit("first-presentation: build manifest does not match the measured binary")
116source_revision = manifest.get("source_revision")
117if not isinstance(source_revision, str) or not re.fullmatch(r"[0-9a-f]{40}", source_revision):
118    raise SystemExit("first-presentation: build manifest does not match the measured binary")
119if vcs_revision is not None and vcs_revision != source_revision:
120    raise SystemExit("first-presentation: build manifest does not match the measured binary")
121modules = manifest.get("modules")
122if not isinstance(modules, dict) or module not in modules:
123    raise SystemExit("first-presentation: build manifest does not match the measured binary")
124entry = modules[module]
125if not isinstance(entry, dict) or entry.get("version") != dep_version:
126    raise SystemExit("first-presentation: build manifest does not match the measured binary")
127revision = entry.get("revision")
128if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}", revision):
129    raise SystemExit("first-presentation: build manifest does not match the measured binary")
130if pseudo_match:
131    if not revision.startswith(pseudo_match.group(2)):
132        raise SystemExit("first-presentation: build manifest does not match the measured binary")
133    tag = pseudo_match.group(1)
134else:
135    tag = dep_version
136
137print("\t".join((binary_sha256, source_revision, dep_version, tag, revision)))
138PY
139}
140
141# Probe the explicitly selected runtime in a separate bounded subprocess.
142# Reports numeric version fields plus library hash, never a path. The
143# candidate must still pass its normal loader ABI/version validation.
144probe_runtime() {
145  timeout --signal=TERM --kill-after=5s 30s python3 "$runtime_probe" --cef-dir "$selected_cef_dir" 2>/dev/null || {
146    echo "first-presentation: CEF runtime probe failed" >&2
147    exit 2
148  }
149}
150
151# The artifact destination is caller-controlled. Never empty or recursively
152# clear it: collection only writes to a newly-created directory whose parent is
153# already canonical and contains no symbolic-link hop.
154prepare_output() {
155  local parent name canonical_parent canonical_output
156
157  [[ -n "$output" ]] || fail_unsafe_output "path must not be empty"
158  [[ "$output" == /* ]] || fail_unsafe_output "path must be absolute"
159  [[ "$output" != "/" ]] || fail_unsafe_output "path must not be /"
160  [[ "${output%/}" != "${HOME%/}" ]] || fail_unsafe_output "path must not be HOME"
161  [[ "$output" != .. && "$output" != ../* && "$output" != */.. && "$output" != */../* ]] || \
162    fail_unsafe_output "path must not contain parent traversal"
163  [[ ! -e "$output" && ! -L "$output" ]] || fail_unsafe_output "path must be a fresh directory"
164
165  parent="$(dirname -- "$output")"
166  name="$(basename -- "$output")"
167  [[ -n "$name" && "$name" != "." && "$name" != ".." ]] || fail_unsafe_output "invalid directory name"
168  [[ -d "$parent" && ! -L "$parent" ]] || fail_unsafe_output "parent must be an existing directory"
169  canonical_parent="$(realpath -e -- "$parent")" || fail_unsafe_output "parent cannot be canonicalized"
170  [[ "$parent" == "$canonical_parent" ]] || fail_unsafe_output "parent must not contain symbolic links"
171
172  mkdir -- "$output" || fail_unsafe_output "could not create fresh directory"
173  canonical_output="$(realpath -e -- "$output")" || fail_unsafe_output "created directory cannot be canonicalized"
174  [[ -d "$output" && ! -L "$output" && "$canonical_output" == "$canonical_parent/$name" ]] || \
175    fail_unsafe_output "created directory changed unexpectedly"
176}
177
178prepare_output
179
180[[ -x "$binary" ]] || { echo "first-presentation: executable not found" >&2; exit 2; }
181[[ -d "$runtime" ]] || { echo "first-presentation: CEF runtime not found" >&2; exit 2; }
182[[ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] || { echo "first-presentation: a current Wayland/X11 display is required" >&2; exit 2; }
183binary_provenance="$(resolve_binary_provenance)" || exit $?
184IFS=$'\t' read -r binary_sha256 measured_source_revision upstream_version upstream_tag upstream_revision <<<"$binary_provenance"
185readonly binary_sha256 measured_source_revision upstream_version upstream_tag upstream_revision
186runtime_probe_json="$(probe_runtime)" || exit $?
187readonly runtime_probe_json
188# Enforce the loader minimum (CHROME_VERSION_MAJOR >= 150) at collection time.
189# This records the observed runtime; it never bypasses the candidate ABI check.
190python3 - "$runtime_probe_json" <<'PY'
191import json, sys
192probe = json.loads(sys.argv[1])
193if not isinstance(probe.get("chrome_version_major"), int) or probe["chrome_version_major"] < 150:
194    raise SystemExit("first-presentation: unsupported CEF runtime")
195PY
196
197# Raw logs and temporary XDG homes may contain machine-local paths. Keep them
198# outside the committed artifact directory and always remove them.
199work_root="$(mktemp -d "${TMPDIR:-/tmp}/dumber-first-presentation.XXXXXX")"
200readonly work_root
201cleanup_work_root() {
202  [[ -n "${work_root:-}" && -d "$work_root" && ! -L "$work_root" ]] || return
203  rm -rf -- "$work_root"
204}
205trap cleanup_work_root EXIT
206python3 - "$output/metadata.json" "$binary_sha256" "$timeout_seconds" "$upstream_module" "$upstream_version" "$upstream_tag" "$upstream_revision" "$measured_source_revision" "$runtime_probe_json" <<'PY'
207import json, os, platform, sys
208path, binary_sha256, timeout, upstream_module, upstream_version, upstream_tag, upstream_revision, measured_source_revision, probe_raw = sys.argv[1:]
209probe = json.loads(probe_raw)
210
211# These deliberately coarse labels support comparison without exposing a host,
212# device name, driver version, path, or environment dump.
213architecture = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}.get(platform.machine().lower(), "other")
214os_label = {"linux": "linux", "darwin": "darwin", "windows": "windows"}.get(platform.system().lower(), "other")
215display_protocol = "wayland" if os.environ.get("WAYLAND_DISPLAY") else "x11"
216gpu_profile = os.environ.get("DUMBER_MACHINE_GPU_PROFILE", "generic-gpu")
217allowed_gpu_profiles = {"generic-gpu", "integrated-gpu", "discrete-gpu", "hybrid-gpu", "virtual-gpu", "unknown-gpu"}
218if gpu_profile not in allowed_gpu_profiles:
219    raise SystemExit("DUMBER_MACHINE_GPU_PROFILE must be a non-identifying profile label")
220
221json.dump({
222  "runs": 5,
223  "runtime": {"label": "cef", "chrome_major": probe["chrome_version_major"], "cef_version_major": probe["cef_version_major"], "libcef_sha256": probe["libcef_sha256"]},
224  "binary": {"label": "dumber", "sha256": binary_sha256},
225  "timeout_seconds": int(timeout),
226  "measured_source_revision": measured_source_revision,
227  "upstream": {"module": upstream_module, "version": upstream_version, "tag": upstream_tag, "revision": upstream_revision},
228  "comparison": {"os": os_label, "architecture": architecture, "display_protocol": display_protocol, "machine_gpu_profile": gpu_profile},
229  "render_configuration": {"backend": "gdk-dmabuf", "buffer_sharing": "dmabuf", "renderer": "vulkan"}
230}, open(path, "w"), indent=2, sort_keys=True)
231PY
232
233for number in $(seq 1 "$runs"); do
234  run="$(printf 'run-%02d' "$number")"
235  root="$work_root/$run"
236  mkdir -p "$root"/{config,data,state,cache}
237  mkdir -p "$root/config/dumber"
238  cat >"$root/config/dumber/config.toml" <<EOF
239[logging]
240level = "debug"
241format = "json"
242enable_file_log = false
243
244[engine.cef]
245cef_dir = "$selected_cef_dir"
246EOF
247  # Every directory below is newly created for this one launch. Do not inherit
248  # a profile, shader cache, CEF root cache, or mutable app configuration.
249  # CEF_DIR is set to exactly the selected runtime, never a conflicting
250  # inherited override.
251  set +e
252  env -i \
253    HOME="$HOME" PATH="$PATH" LANG="${LANG:-C.UTF-8}" \
254    WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-}" DISPLAY="${DISPLAY:-}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-}" \
255    DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-}" XAUTHORITY="${XAUTHORITY:-}" \
256    XDG_CONFIG_HOME="$root/config" XDG_DATA_HOME="$root/data" XDG_STATE_HOME="$root/state" XDG_CACHE_HOME="$root/cache" \
257    CEF_DIR="$selected_cef_dir" DUMBER_CEF_DIR="$selected_cef_dir" \
258    DUMBER_CEF_ROOT_CACHE_PATH="$root/cef-root-cache" DUMBER_RENDER_STACK="vulkan-dmabuf" \
259    PUREGO_CEF2GTK_BACKEND="gdk-dmabuf" PUREGO_CEF2GTK_ANGLE_BACKEND="vulkan" GSK_RENDERER="vulkan" \
260    timeout --signal=TERM --kill-after=5s "${timeout_seconds}s" "$binary" browse about:blank >"$root/process.log" 2>&1
261  set -e
262  # A completed, valid first-presentation summary is the observation success
263  # signal. The GUI may later be stopped by timeout or terminate independently.
264  python3 - "$root/process.log" "$output/$run.json" "$run" <<'PY'
265import json, sys
266log, destination, run = sys.argv[1:]
267order = ["process_entry", "config_complete", "cef_library_load_begin", "cef_initialized", "browser_create_requested", "first_accelerated_paint_received", "first_dmabuf_texture_swap", "first_gtk_presentation"]
268records, summary = [], None
269for line in open(log, errors="replace"):
270    try: event = json.loads(line)
271    except json.JSONDecodeError: continue
272    if event.get("message") == "startup_trace: milestone":
273        records.append({key: event.get(key) for key in ("milestone", "t_ms", "delta_ms")})
274    if event.get("message") == "startup_trace: first presentation":
275        summary = {key: event.get(key) for key in ("backend", "incomplete_reason", "total_ms")}
276names = [event.get("milestone") for event in records]
277times = [event.get("t_ms") for event in records]
278deltas = [event.get("delta_ms") for event in records]
279valid = names == order and all(type(t) is int for t in times) and times == sorted(times)
280valid = valid and all(type(delta) is int for delta in deltas)
281valid = valid and all(delta == current - previous for previous, current, delta in zip([0] + times, times, deltas))
282valid = valid and summary is not None and summary.get("backend") == "gdk-dmabuf" and not summary.get("incomplete_reason")
283valid = valid and type(summary.get("total_ms")) is int and summary["total_ms"] == times[-1]
284result = {"run": run, "valid": valid, "milestones": records, "summary": summary}
285json.dump(result, open(destination, "w"), indent=2)
286if not valid:
287    raise SystemExit(f"{run}: invalid or incomplete non-DMABUF timeline")
288PY
289done
290
291python3 - "$output" <<'PY'
292import json, math, statistics, sys
293root = sys.argv[1]
294runs = [json.load(open(f"{root}/run-{n:02d}.json")) for n in range(1, 6)]
295if len(runs) != 5 or not all(run["valid"] for run in runs): raise SystemExit("expected exactly five valid runs")
296totals = sorted(run["summary"]["total_ms"] for run in runs)
297p95 = totals[max(0, math.ceil(.95 * len(totals)) - 1)]
298json.dump({"runs": 5, "total_ms": {"min": totals[0], "median": statistics.median(totals), "max": totals[-1], "p95": p95}}, open(f"{root}/baseline.json", "w"), indent=2)
299PY
300
301echo "first-presentation artifacts: $output"

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close