v0.31.0 bf0756b

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 runtime="${DUMBER_CEF_DIR:-$HOME/.local/share/cef-147-runtime}"
10readonly binary="${DUMBER_FIRST_PRESENTATION_BIN:-$PWD/dist/dumber}"
11
12fail_unsafe_output() {
13  echo "first-presentation: unsafe output path: $1" >&2
14  exit 2
15}
16
17if [[ -v DUMBER_FIRST_PRESENTATION_OUTPUT ]]; then
18  output="$DUMBER_FIRST_PRESENTATION_OUTPUT"
19else
20  # Evidence is state, not source: keep the default outside the checkout and
21  # make every invocation a fresh child of the external evidence directory.
22  readonly state_home="${XDG_STATE_HOME:-$HOME/.local/state}"
23  [[ "$state_home" == /* ]] || fail_unsafe_output "XDG_STATE_HOME must be absolute"
24  readonly evidence_root="$state_home/dumber/roadmap-evidence"
25  mkdir -p -- "$evidence_root" || fail_unsafe_output "could not create XDG state directory"
26  canonical_evidence_root="$(realpath -e -- "$evidence_root")" || \
27    fail_unsafe_output "XDG state directory cannot be canonicalized"
28  [[ "$canonical_evidence_root" == "$evidence_root" && ! -L "$evidence_root" ]] || \
29    fail_unsafe_output "XDG state directory must not contain symbolic links"
30  output="$evidence_root/first-presentation-$(date -u +%Y%m%dT%H%M%S)-$$"
31fi
32readonly output
33readonly upstream_module="github.com/bnema/purego-cef2gtk"
34
35# Resolve the version selected by this checkout, then obtain its immutable VCS
36# origin from Go's cached module metadata. Do not infer a revision from a tag,
37# a branch, or a truncated pseudo-version suffix.
38resolve_upstream_provenance() {
39  local selected_metadata selected_version downloaded_metadata
40
41  selected_metadata="$(GOFLAGS=-mod=mod go list -m -json "$upstream_module" 2>/dev/null)" || {
42    echo "first-presentation: immutable module provenance is unavailable" >&2
43    exit 2
44  }
45  selected_version="$(python3 - "$upstream_module" "$selected_metadata" <<'PY'
46import json, sys
47try:
48    metadata = json.loads(sys.argv[2])
49    if metadata.get("Path") != sys.argv[1] or not isinstance(metadata.get("Version"), str):
50        raise ValueError
51    print(metadata["Version"])
52except (AttributeError, TypeError, ValueError, json.JSONDecodeError):
53    raise SystemExit("first-presentation: immutable module provenance is unavailable")
54PY
55)" || exit $?
56  downloaded_metadata="$(go mod download -json "$upstream_module@$selected_version" 2>/dev/null)" || {
57    echo "first-presentation: immutable module provenance is unavailable" >&2
58    exit 2
59  }
60
61  python3 - "$upstream_module" "$selected_metadata" "$downloaded_metadata" <<'PY'
62import json, re, sys
63
64module, selected_raw, downloaded_raw = sys.argv[1:]
65
66def fail():
67    # Do not expose Go cache locations or other machine-local values.
68    raise SystemExit("first-presentation: immutable module provenance is unavailable")
69
70try:
71    selected = json.loads(selected_raw)
72    downloaded = json.loads(downloaded_raw)
73    version = selected["Version"]
74    if selected.get("Path") != module or downloaded.get("Path") != module:
75        fail()
76    if downloaded.get("Version") != version:
77        fail()
78    # A pseudo-version identifies an immutable commit; a released semver tag is
79    # accepted only when Go records that exact tag and its immutable hash.
80    pseudo_match = re.fullmatch(r"(v\d+\.\d+\.\d+)-0\.\d{14}-([0-9a-f]{12})", version)
81    tag_match = re.fullmatch(r"v\d+\.\d+\.\d+", version)
82    if not pseudo_match and not tag_match:
83        fail()
84    info_path = downloaded.get("Info")
85    if not isinstance(info_path, str) or not info_path:
86        fail()
87    with open(info_path, encoding="utf-8") as info_file:
88        info = json.load(info_file)
89    origin = info.get("Origin") or downloaded.get("Origin")
90    if info.get("Version") != version or not isinstance(origin, dict):
91        fail()
92    revision = origin.get("Hash")
93    if origin.get("VCS") != "git" or origin.get("URL") != "https://github.com/bnema/purego-cef2gtk":
94        fail()
95    if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}", revision):
96        fail()
97    if pseudo_match:
98        if not revision.startswith(pseudo_match.group(2)):
99            fail()
100        # A named ref can move. Only the immutable hash itself is acceptable.
101        if origin.get("Ref") not in (None, "", revision):
102            fail()
103        tag = pseudo_match.group(1)
104    else:
105        if origin.get("Ref") != "refs/tags/" + version:
106            fail()
107        tag = version
108except (AttributeError, KeyError, OSError, TypeError, ValueError, json.JSONDecodeError):
109    fail()
110
111print("\t".join((version, tag, revision)))
112PY
113}
114
115# The artifact destination is caller-controlled. Never empty or recursively
116# clear it: collection only writes to a newly-created directory whose parent is
117# already canonical and contains no symbolic-link hop.
118prepare_output() {
119  local parent name canonical_parent canonical_output
120
121  [[ -n "$output" ]] || fail_unsafe_output "path must not be empty"
122  [[ "$output" == /* ]] || fail_unsafe_output "path must be absolute"
123  [[ "$output" != "/" ]] || fail_unsafe_output "path must not be /"
124  [[ "${output%/}" != "${HOME%/}" ]] || fail_unsafe_output "path must not be HOME"
125  [[ "$output" != .. && "$output" != ../* && "$output" != */.. && "$output" != */../* ]] || \
126    fail_unsafe_output "path must not contain parent traversal"
127  [[ ! -e "$output" && ! -L "$output" ]] || fail_unsafe_output "path must be a fresh directory"
128
129  parent="$(dirname -- "$output")"
130  name="$(basename -- "$output")"
131  [[ -n "$name" && "$name" != "." && "$name" != ".." ]] || fail_unsafe_output "invalid directory name"
132  [[ -d "$parent" && ! -L "$parent" ]] || fail_unsafe_output "parent must be an existing directory"
133  canonical_parent="$(realpath -e -- "$parent")" || fail_unsafe_output "parent cannot be canonicalized"
134  [[ "$parent" == "$canonical_parent" ]] || fail_unsafe_output "parent must not contain symbolic links"
135
136  mkdir -- "$output" || fail_unsafe_output "could not create fresh directory"
137  canonical_output="$(realpath -e -- "$output")" || fail_unsafe_output "created directory cannot be canonicalized"
138  [[ -d "$output" && ! -L "$output" && "$canonical_output" == "$canonical_parent/$name" ]] || \
139    fail_unsafe_output "created directory changed unexpectedly"
140}
141
142prepare_output
143
144[[ -x "$binary" ]] || { echo "first-presentation: executable not found: $binary" >&2; exit 2; }
145[[ -d "$runtime" ]] || { echo "first-presentation: CEF runtime not found: $runtime" >&2; exit 2; }
146[[ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] || { echo "first-presentation: a current Wayland/X11 display is required" >&2; exit 2; }
147upstream_provenance="$(resolve_upstream_provenance)" || exit $?
148IFS=$'\t' read -r upstream_version upstream_tag upstream_revision <<<"$upstream_provenance"
149readonly upstream_version upstream_tag upstream_revision
150
151# Raw logs and temporary XDG homes may contain machine-local paths. Keep them
152# outside the committed artifact directory and always remove them.
153work_root="$(mktemp -d "${TMPDIR:-/tmp}/dumber-first-presentation.XXXXXX")"
154readonly work_root
155cleanup_work_root() {
156  [[ -n "${work_root:-}" && -d "$work_root" && ! -L "$work_root" ]] || return
157  rm -rf -- "$work_root"
158}
159trap cleanup_work_root EXIT
160python3 - "$output/metadata.json" "$binary" "$timeout_seconds" "$upstream_module" "$upstream_version" "$upstream_tag" "$upstream_revision" <<'PY'
161import hashlib, json, os, platform, subprocess, sys
162path, binary, timeout, upstream_module, upstream_version, upstream_tag, upstream_revision = sys.argv[1:]
163with open(binary, "rb") as candidate:
164  binary_sha256 = hashlib.file_digest(candidate, "sha256").hexdigest()
165
166# These deliberately coarse labels support comparison without exposing a host,
167# device name, driver version, path, or environment dump.
168architecture = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}.get(platform.machine().lower(), "other")
169os_label = {"linux": "linux", "darwin": "darwin", "windows": "windows"}.get(platform.system().lower(), "other")
170display_protocol = "wayland" if os.environ.get("WAYLAND_DISPLAY") else "x11"
171gpu_profile = os.environ.get("DUMBER_MACHINE_GPU_PROFILE", "generic-gpu")
172allowed_gpu_profiles = {"generic-gpu", "integrated-gpu", "discrete-gpu", "hybrid-gpu", "virtual-gpu", "unknown-gpu"}
173if gpu_profile not in allowed_gpu_profiles:
174    raise SystemExit("DUMBER_MACHINE_GPU_PROFILE must be a non-identifying profile label")
175
176json.dump({
177  "runs": 5,
178  "runtime": {"label": "cef", "version": "147"},
179  "binary": {"label": "dumber", "sha256": binary_sha256},
180  "timeout_seconds": int(timeout),
181  "measured_source_revision": subprocess.check_output(["git", "-c", f"safe.directory={os.getcwd()}", "rev-parse", "HEAD"], text=True).strip(),
182  "upstream": {"module": upstream_module, "version": upstream_version, "tag": upstream_tag, "revision": upstream_revision},
183  "comparison": {"os": os_label, "architecture": architecture, "display_protocol": display_protocol, "machine_gpu_profile": gpu_profile},
184  "render_configuration": {"backend": "gdk-dmabuf", "buffer_sharing": "dmabuf", "renderer": "vulkan"}
185}, open(path, "w"), indent=2, sort_keys=True)
186PY
187
188for number in $(seq 1 "$runs"); do
189  run="$(printf 'run-%02d' "$number")"
190  root="$work_root/$run"
191  mkdir -p "$root"/{config,data,state,cache}
192  mkdir -p "$root/config/dumber"
193  cat >"$root/config/dumber/config.toml" <<EOF
194[logging]
195level = "debug"
196format = "json"
197enable_file_log = false
198
199[engine.cef]
200cef_dir = "$runtime"
201EOF
202  # Every directory below is newly created for this one launch. Do not inherit
203  # a profile, shader cache, CEF root cache, or mutable app configuration.
204  set +e
205  env -i \
206    HOME="$HOME" PATH="$PATH" LANG="${LANG:-C.UTF-8}" \
207    WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-}" DISPLAY="${DISPLAY:-}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-}" \
208    DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-}" XAUTHORITY="${XAUTHORITY:-}" \
209    XDG_CONFIG_HOME="$root/config" XDG_DATA_HOME="$root/data" XDG_STATE_HOME="$root/state" XDG_CACHE_HOME="$root/cache" \
210    DUMBER_CEF_ROOT_CACHE_PATH="$root/cef-root-cache" DUMBER_RENDER_STACK="vulkan-dmabuf" \
211    PUREGO_CEF2GTK_BACKEND="gdk-dmabuf" PUREGO_CEF2GTK_ANGLE_BACKEND="vulkan" GSK_RENDERER="vulkan" \
212    timeout --signal=TERM --kill-after=5s "${timeout_seconds}s" "$binary" browse about:blank >"$root/process.log" 2>&1
213  set -e
214  # A completed, valid first-presentation summary is the observation success
215  # signal. The GUI may later be stopped by timeout or terminate independently.
216  python3 - "$root/process.log" "$output/$run.json" "$run" <<'PY'
217import json, sys
218log, destination, run = sys.argv[1:]
219order = ["process_entry", "config_complete", "cef_library_load_begin", "cef_initialized", "browser_create_requested", "first_accelerated_paint_received", "first_dmabuf_texture_swap", "first_gtk_presentation"]
220records, summary = [], None
221for line in open(log, errors="replace"):
222    try: event = json.loads(line)
223    except json.JSONDecodeError: continue
224    if event.get("message") == "startup_trace: milestone":
225        records.append({key: event.get(key) for key in ("milestone", "t_ms", "delta_ms")})
226    if event.get("message") == "startup_trace: first presentation":
227        summary = {key: event.get(key) for key in ("backend", "incomplete_reason", "total_ms")}
228names = [event.get("milestone") for event in records]
229times = [event.get("t_ms") for event in records]
230deltas = [event.get("delta_ms") for event in records]
231valid = names == order and all(type(t) is int for t in times) and times == sorted(times)
232valid = valid and all(type(delta) is int for delta in deltas)
233valid = valid and all(delta == current - previous for previous, current, delta in zip([0] + times, times, deltas))
234valid = valid and summary is not None and summary.get("backend") == "gdk-dmabuf" and not summary.get("incomplete_reason")
235valid = valid and type(summary.get("total_ms")) is int and summary["total_ms"] == times[-1]
236result = {"run": run, "valid": valid, "milestones": records, "summary": summary}
237json.dump(result, open(destination, "w"), indent=2)
238if not valid:
239    raise SystemExit(f"{run}: invalid or incomplete non-DMABUF timeline")
240PY
241done
242
243python3 - "$output" <<'PY'
244import json, math, statistics, sys
245root = sys.argv[1]
246runs = [json.load(open(f"{root}/run-{n:02d}.json")) for n in range(1, 6)]
247if len(runs) != 5 or not all(run["valid"] for run in runs): raise SystemExit("expected exactly five valid runs")
248totals = sorted(run["summary"]["total_ms"] for run in runs)
249p95 = totals[max(0, math.ceil(.95 * len(totals)) - 1)]
250json.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)
251PY
252
253echo "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