@gjsify/adwaita-gobject-native 0.64.1-next.241 → 0.64.1-next.243

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.
package/README.md CHANGED
@@ -85,6 +85,27 @@ drives the same `GOBJECT_VECTORS` against node-gi directly. The pair is the poin
85
85
  what the right answer is on this binding, this package says the subset still gives it with a real
86
86
  store underneath.
87
87
 
88
+ ## Per-instance cost
89
+
90
+ `src/bench.ts` times four rows against a registered class with two properties and one signal:
91
+ `construct`, `construct+first-read`, `set+notify` and `emit`. It is exported from `index.ts` and
92
+ takes its engine as an argument, so ONE process times both engines and the pure-JS leg cannot be a
93
+ different build of anything. `src/bench.node-gi.mts` is the host entry:
94
+
95
+ ```sh
96
+ gjsify workspace @gjsify/adwaita-gobject-native run bench:node-gi
97
+ ```
98
+
99
+ `GJSIFY_BENCH_ITERATIONS`, `GJSIFY_BENCH_REPEATS` and `GJSIFY_BENCH_WARMUPS` tune it. The device
100
+ leg (`tests/integration/nativescript-gi/app/specs/bench.ts`, behind `GJSIFY_GI_BENCH=1`) calls the
101
+ same body, so host and device cannot drift. A script on demand, not a CI gate: a timing threshold
102
+ in CI fails on whatever the runner was doing that minute.
103
+
104
+ `construct+first-read` exists because the engine builds its GObject twin LAZILY, in `twinOf` on
105
+ first use. `construct` alone touches no engine for plain `READWRITE` properties, so timing only
106
+ that reports a cost of zero and is wrong. Numbers for both hosts:
107
+ `docs/reports/2026-10-15-adr-0110-gates.md`.
108
+
88
109
  ## What is NOT measured
89
110
 
90
111
  Nothing here has run on Android. `@gjsify/adwaita-nativescript`'s `createGObject(door)` does not
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{PURE_JS_ENGINE as e,createGObject as t}from"@gjsify/adwaita-core";function selectTimer(){let e=globalThis.performance;if(typeof e?.now==`function`){let t=e.now.bind(e);return{name:`performance.now`,nowNs:()=>t()*1e6}}return{name:`Date.now`,nowNs:()=>Date.now()*1e6}}function median(e){let t=[...e].sort((e,t)=>e-t),n=t.length>>1;return t.length%2==1?t[n]:(t[n-1]+t[n])/2}const n={name:`bench door`,dispatch(e,t){throw Error(`bench door: nothing to dispatch '${t}' on`)},listen(e,t){throw Error(`bench door: nothing to listen '${t}' on`)},createFromTree(e){throw Error(`bench door: the benchmark registers no template`)},attach(){},register(){}};function benchClassFor(e){let r=t(n,e),{ParamSpec:i,ParamFlags:a}=r;return r.registerClass({GTypeName:`BenchSubject`,Properties:{label:i.string(`label`,``,``,a.READWRITE,``),active:i.boolean(`active`,``,``,a.READWRITE,!1),count:i.int(`count`,``,``,a.READWRITE,0,1e6,0)},Signals:{changed:{param_types:[r.TYPE_STRING]},ticked:{param_types:[]}}},class extends r.Object{})}function measure(e,t,n,r,i,a){for(let e=0;e<r;e++)a(t,-1-e);let o=[],s=[];for(let e=0;e<n;e++){let n=i();a(t,e);let r=i()-n;s.push(r),o.push(r/t)}return{name:e,iterations:t,repeats:n,medianNs:median(o),minNs:Math.min(...o),maxNs:Math.max(...o),medianMs:median(s)/1e6}}function runGObjectBench(e,t={}){let n=t.iterations??2e4,r=t.repeats??7,i=t.warmups??2,{name:a,nowNs:o}=selectTimer(),s=benchClassFor(e),c=[],l=measure(`construct`,n,r,i,o,e=>{let t=Array.from({length:e});for(let n=0;n<e;n++)t[n]=new s;c=t}),u=measure(`construct+first-read`,n,r,i,o,e=>{let t=Array.from({length:e});for(let n=0;n<e;n++){let e=new s;sink(e.label),t[n]=e}c=t}),d=new s,f=0,p=0;d.connect(`notify::label`,()=>{f++}),d.connect(`changed`,()=>{p++});let m=measure(`set+notify`,n,r,i,o,e=>{let t=d;for(let n=0;n<e;n++)t.label=n%2==0?`a`:`b`}),h=measure(`emit`,n,r,i,o,e=>{for(let t=0;t<e;t++)d.emit(`changed`,`x`)});return sink(f+p+c.length),{engine:e.name,timer:a,cases:[l,u,m,h]}}let r;function sink(e){if(r=e,r===Symbol.for(`@gjsify/bench/never`))throw Error(`unreachable`)}function runPureJsBench(t){return runGObjectBench(e,t)}function round(e){return e>=100?e.toFixed(0):e>=10?e.toFixed(1):e.toFixed(2)}function formatBenchLines(e){return[`engine=${e.engine} timer=${e.timer}`,...e.cases.map(e=>`${e.name}: median ${round(e.medianNs)} ns/op (min ${round(e.minNs)}, max ${round(e.maxNs)}; ${e.iterations} ops x ${e.repeats} repeats, median repeat ${round(e.medianMs)} ms)`)]}function benchRatios(e,t){let n=[];for(let r of t.cases){let t=e.cases.find(e=>e.name===r.name);t&&t.medianNs>0&&n.push({name:r.name,ratio:r.medianNs/t.medianNs})}return n}export{benchRatios,formatBenchLines,runGObjectBench,runPureJsBench};
package/lib/esm/engine.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{bindProperties as e}from"@gjsify/adwaita-core";const t={gchararray:`string`,gboolean:`boolean`,gint:`int`,guint:`uint`,gdouble:`double`},n={bidirectional:`BIDIRECTIONAL`,"sync-create":`SYNC_CREATE`,"invert-boolean":`INVERT_BOOLEAN`},r={int:[-2147483648,2147483647],uint:[0,4294967295],double:[-Number.MAX_VALUE,Number.MAX_VALUE]};function aliasesOf(e){return[e,e.replace(/-/g,`_`),e.replace(/-([a-z])/g,(e,t)=>t.toUpperCase())]}function createRealGioEngine(i){let a=i(`GObject`,`2.0`),o=a.ParamFlags.READWRITE,s=new WeakMap,c=new WeakMap,l=new WeakMap,u=new WeakMap,d=new WeakMap,f=new WeakMap,p=0;function touch(e,t){let n=f.get(e);n||f.set(e,n=new Set);let r=!n.has(t);return n.add(t),r}function uniqueTypeName(e){return`${e.replace(/[^A-Za-z0-9_+-]/g,`_`).replace(/^[^A-Za-z_]/,`_`)||`_`}_RealGio${++p}`}function paramSpecFor(e){let{name:t,nick:n,blurb:i}=e,s=e.kind===`string`||e.kind===`boolean`?void 0:r[e.kind],c=e.minimum??s?.[0]??0,l=e.maximum??s?.[1]??0,u=e.get_default_value();switch(e.kind){case`boolean`:return a.ParamSpec.boolean(t,n,i,o,u);case`string`:return a.ParamSpec.string(t,n,i,o,u??void 0);case`int`:return a.ParamSpec.int(t,n,i,o,c,l,u);case`uint`:return a.ParamSpec.uint(t,n,i,o,c,l,u);case`double`:return a.ParamSpec.double(t,n,i,o,c,l,u)}}function signalTypeOf(e){let n=t[e.name];if(n===void 0)throw TypeError(`real-gio engine: no GObject signal parameter type for '${e.name}'`);return n}function parentTwinOf(e){for(let t=Object.getPrototypeOf(e);typeof t==`function`;){let e=s.get(t);if(e)return e;t=Object.getPrototypeOf(t)}return a.Object}function registeredChainOf(e){let t=[];for(let n=e.constructor;typeof n==`function`;){let e=c.get(n);e&&t.push(e),n=Object.getPrototypeOf(n)}return t}function twinClassOf(e){for(let t=e.constructor;typeof t==`function`;){let e=s.get(t);if(e)return e;t=Object.getPrototypeOf(t)}}function twinOf(e){let t=l.get(e);if(t)return t;let n=twinClassOf(e);if(!n){let t=e.constructor?.name??String(e);throw Error(`real-gio engine: '${t}' was never registered against this engine`)}let r=new n;return l.set(e,r),r}function specOf(e,t){for(let n of registeredChainOf(e))for(let e of n.properties)if(e.name===t)return e}function canonicalPropertyOf(e,t){for(let n of registeredChainOf(e))for(let e of n.properties)if(aliasesOf(e.name).includes(t))return e.name;return t}function withEmission(e,t,n){let r=d.get(e);r||d.set(e,r=[]),r.push({key:t,stopped:!1});try{n()}finally{r.pop()}}return{name:`real-gio`,register(e){let t=parentTwinOf(e.klass),n={};for(let t of e.properties)n[t.name]=paramSpecFor(t);let r={};for(let[t,n]of Object.entries(e.signals))r[t]={param_types:n.map(signalTypeOf)};s.set(e.klass,a.registerClass({GTypeName:uniqueTypeName(e.typeName),Properties:n,Signals:r},class extends t{})),c.set(e.klass,e)},getValue(e,t){let n=twinOf(e);return touch(e,t.name),n[t.name]},setValue(e,t,n){let r=twinOf(e);return!touch(e,t.name)&&r[t.name]===n||withEmission(e,`notify::${t.name}`,()=>{r.set_property(t.name,n)}),!1},addHandler(e,t,n,r){let i=twinOf(e),a=n.startsWith(`notify::`)?specOf(e,n.slice(8)):void 0,o=i.connect(n,(t,...n)=>a===void 0?r(e,...n):r(e,a)),s=u.get(e);s||u.set(e,s=new Map),s.set(t,o)},removeHandler(e,t){let n=u.get(e),r=n?.get(t);return r===void 0?!1:(n.delete(t),twinOf(e).disconnect(r),!0)},emit(e,t,n){withEmission(e,t,()=>{twinOf(e).emit(t,...n)})},notify(e,t){withEmission(e,`notify::${t.name}`,()=>{twinOf(e).notify(t.name)})},stopEmission(e,t){let n=d.get(e)??[];for(let r=n.length-1;r>=0;r--){let i=n[r];if(i.key===t&&!i.stopped)return i.stopped=!0,a.signal_stop_emission_by_name(twinOf(e),t),!0}return!1},bind(t,r,i,o,s){if(!twinClassOf(t)||!twinClassOf(i))return e(t,r,i,o,s);let c=s.reduce((e,t)=>e|(a.BindingFlags[n[t]]??0),a.BindingFlags.DEFAULT??0),l=twinOf(t).bind_property(canonicalPropertyOf(t,r),twinOf(i),canonicalPropertyOf(i,o),c);return{unbind:()=>l.unbind()}}}}export{createRealGioEngine};
1
+ import"./_virtual/_rolldown/runtime.js";import{bindProperties as e}from"@gjsify/adwaita-core";const t={gchararray:`string`,gboolean:`boolean`,gint:`int`,guint:`uint`,gdouble:`double`},n={bidirectional:`BIDIRECTIONAL`,"sync-create":`SYNC_CREATE`,"invert-boolean":`INVERT_BOOLEAN`},r={int:[-2147483648,2147483647],uint:[0,4294967295],double:[-Number.MAX_VALUE,Number.MAX_VALUE]};function aliasesOf(e){return[e,e.replace(/-/g,`_`),e.replace(/-([a-z])/g,(e,t)=>t.toUpperCase())]}function createRealGioEngine(i){let a=i(`GObject`,`2.0`),o=a.ParamFlags.READWRITE,s=new WeakMap,c=new WeakMap,l=new WeakMap,u=new WeakMap,d=new WeakMap,f=new WeakMap,p=0;function touch(e,t){let n=f.get(e);n||f.set(e,n=new Set);let r=!n.has(t);return n.add(t),r}function uniqueTypeName(e){return`${e.replace(/[^A-Za-z0-9_+-]/g,`_`).replace(/^[^A-Za-z_]/,`_`)||`_`}_RealGio${++p}`}function paramSpecFor(e){let{name:t,nick:n,blurb:i}=e,s=e.kind===`string`||e.kind===`boolean`?void 0:r[e.kind],c=e.minimum??s?.[0]??0,l=e.maximum??s?.[1]??0,u=e.get_default_value();switch(e.kind){case`boolean`:return a.ParamSpec.boolean(t,n,i,o,u);case`string`:return a.ParamSpec.string(t,n,i,o,u??void 0);case`int`:return a.ParamSpec.int(t,n,i,o,c,l,u);case`uint`:return a.ParamSpec.uint(t,n,i,o,c,l,u);case`double`:return a.ParamSpec.double(t,n,i,o,c,l,u)}}function signalTypeOf(e){let n=t[e.name];if(n===void 0)throw TypeError(`real-gio engine: no GObject signal parameter type for '${e.name}'`);return n}function parentTwinOf(e){for(let t=Object.getPrototypeOf(e);typeof t==`function`;){let e=s.get(t);if(e)return e;t=Object.getPrototypeOf(t)}return a.Object}function registeredChainOf(e){let t=[];for(let n=e.constructor;typeof n==`function`;){let e=c.get(n);e&&t.push(e),n=Object.getPrototypeOf(n)}return t}function twinClassOf(e){for(let t=e.constructor;typeof t==`function`;){let e=s.get(t);if(e)return e;t=Object.getPrototypeOf(t)}}function twinOf(e){let t=l.get(e);if(t)return t;let n=twinClassOf(e);if(!n){let t=e.constructor?.name??String(e);throw Error(`real-gio engine: '${t}' was never registered against this engine`)}let r=new n;return l.set(e,r),r}function specOf(e,t){for(let n of registeredChainOf(e))for(let e of n.properties)if(e.name===t)return e}function canonicalPropertyOf(e,t){for(let n of registeredChainOf(e))for(let e of n.properties)if(aliasesOf(e.name).includes(t))return e.name;return t}function withEmission(e,t,n){let r=d.get(e);r||d.set(e,r=[]),r.push({key:t,stopped:!1});try{n()}finally{r.pop()}}return{name:`real-gio`,register(e){let t=parentTwinOf(e.klass),n={};for(let t of e.properties)n[t.name]=paramSpecFor(t);let r={};for(let[t,n]of Object.entries(e.signals))r[t]={param_types:n.map(signalTypeOf)};s.set(e.klass,a.registerClass({GTypeName:uniqueTypeName(e.typeName),Properties:n,Signals:r},class extends t{})),c.set(e.klass,e)},getValue(e,t){let n=twinOf(e);return touch(e,t.name),n[t.name]},setValue(e,t,n){let r=twinOf(e);return!touch(e,t.name)&&r[t.name]===n||withEmission(e,`notify::${t.name}`,()=>{r[t.name]=n}),!1},addHandler(e,t,n,r){let i=twinOf(e),a=n.startsWith(`notify::`)?specOf(e,n.slice(8)):void 0,o=i.connect(n,(t,...n)=>a===void 0?r(e,...n):r(e,a)),s=u.get(e);s||u.set(e,s=new Map),s.set(t,o)},removeHandler(e,t){let n=u.get(e),r=n?.get(t);return r===void 0?!1:(n.delete(t),twinOf(e).disconnect(r),!0)},emit(e,t,n){withEmission(e,t,()=>{twinOf(e).emit(t,...n)})},notify(e,t){withEmission(e,`notify::${t.name}`,()=>{twinOf(e).notify(t.name)})},stopEmission(e,t){let n=d.get(e)??[];for(let r=n.length-1;r>=0;r--){let i=n[r];if(i.key===t&&!i.stopped)return i.stopped=!0,a.signal_stop_emission_by_name(twinOf(e),t),!0}return!1},bind(t,r,i,o,s){if(!twinClassOf(t)||!twinClassOf(i))return e(t,r,i,o,s);let c=s.reduce((e,t)=>e|(a.BindingFlags[n[t]]??0),a.BindingFlags.DEFAULT??0),l=twinOf(t).bind_property(canonicalPropertyOf(t,r),twinOf(i),canonicalPropertyOf(i,o),c);return{unbind:()=>l.unbind()}}}}export{createRealGioEngine};
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{createRealGioEngine as e}from"./engine.js";import{createRealGioFileChooserSubject as t,rowsFor as n}from"./file-chooser-subject.js";import{useRealGObjectEngine as r}from"./opt-in.js";import{createRealGioListStore as i}from"./list-store.js";import{SUBSET_SUBJECT_NAME as a,createSubsetSubject as o}from"./vector-subject.js";import{createRealGioSettingsSubject as s}from"./settings-subject.js";export{a as SUBSET_SUBJECT_NAME,e as createRealGioEngine,t as createRealGioFileChooserSubject,i as createRealGioListStore,s as createRealGioSettingsSubject,o as createSubsetSubject,n as rowsFor,r as useRealGObjectEngine};
1
+ import{benchRatios as e,formatBenchLines as t,runGObjectBench as n,runPureJsBench as r}from"./bench.js";import{createRealGioEngine as i}from"./engine.js";import{createRealGioFileChooserSubject as a,rowsFor as o}from"./file-chooser-subject.js";import{useRealGObjectEngine as s}from"./opt-in.js";import{createRealGioListStore as c}from"./list-store.js";import{SUBSET_SUBJECT_NAME as l,createSubsetSubject as u}from"./vector-subject.js";import{createRealGioSettingsSubject as d}from"./settings-subject.js";export{l as SUBSET_SUBJECT_NAME,e as benchRatios,i as createRealGioEngine,a as createRealGioFileChooserSubject,c as createRealGioListStore,d as createRealGioSettingsSubject,u as createSubsetSubject,t as formatBenchLines,o as rowsFor,n as runGObjectBench,r as runPureJsBench,s as useRealGObjectEngine};
@@ -0,0 +1,51 @@
1
+ import type { GObjectEngine } from '@gjsify/adwaita-core';
2
+ export interface BenchOptions {
3
+ /** Operations per repeat. Default 20 000 — large enough to swamp the clock, small enough to hold. */
4
+ iterations?: number;
5
+ /** Timed repeats, the population the median and the spread come from. Default 7. */
6
+ repeats?: number;
7
+ /** Repeats run and DISCARDED first, so the numbers are of a warm JIT. Default 2. */
8
+ warmups?: number;
9
+ }
10
+ export interface BenchCase {
11
+ /** `construct`, `construct+first-read`, `set+notify` or `emit`. */
12
+ readonly name: string;
13
+ readonly iterations: number;
14
+ readonly repeats: number;
15
+ /** Median over the repeats, nanoseconds per operation — the number to quote. */
16
+ readonly medianNs: number;
17
+ /** Fastest and slowest repeat, same unit; the spread. */
18
+ readonly minNs: number;
19
+ readonly maxNs: number;
20
+ /** The median repeat's wall time, for a sense of what the row actually ran. */
21
+ readonly medianMs: number;
22
+ }
23
+ export interface BenchResult {
24
+ /** The engine's own `name` — `pure-js` or `real-gio`. */
25
+ readonly engine: string;
26
+ /** Which clock answered, so a run with a 1 ms clock is not mistaken for a precise one. */
27
+ readonly timer: string;
28
+ readonly cases: readonly BenchCase[];
29
+ }
30
+ /**
31
+ * Times the four rows against `engine` and returns the numbers. Takes the engine rather than
32
+ * reading the core's slot, so one process can measure both — the slot freezes on first read and
33
+ * would pin a process to one engine.
34
+ */
35
+ export declare function runGObjectBench(engine: GObjectEngine, options?: BenchOptions): BenchResult;
36
+ /** `runGObjectBench` against the default engine, for the caller that wants the comparison leg. */
37
+ export declare function runPureJsBench(options?: BenchOptions): BenchResult;
38
+ /**
39
+ * The result as lines a report can paste: one per row, nanoseconds per operation with the spread.
40
+ * Deliberately plain text — both legs print through a different channel (stdout on the host, the
41
+ * suite's `INFO` markers on the device) and neither wants a table.
42
+ */
43
+ export declare function formatBenchLines(result: BenchResult): string[];
44
+ /**
45
+ * The real engine's median divided by the pure-JS one's, per row — the ratio ADR 0110's gate is
46
+ * actually about. Rows the two runs do not share are skipped.
47
+ */
48
+ export declare function benchRatios(pureJs: BenchResult, realGio: BenchResult): {
49
+ name: string;
50
+ ratio: number;
51
+ }[];
@@ -4,6 +4,8 @@ export type { RequireGi } from './engine.js';
4
4
  export { createRealGioListStore } from './list-store.js';
5
5
  export type { RealGioListStore } from './list-store.js';
6
6
  export { createSubsetSubject, SUBSET_SUBJECT_NAME } from './vector-subject.js';
7
+ export { benchRatios, formatBenchLines, runGObjectBench, runPureJsBench } from './bench.js';
8
+ export type { BenchCase, BenchOptions, BenchResult } from './bench.js';
7
9
  export { createRealGioSettingsSubject } from './settings-subject.js';
8
10
  export type { RealGioSettingsOptions, RealGioSettingsSubject } from './settings-subject.js';
9
11
  export { createRealGioFileChooserSubject, rowsFor } from './file-chooser-subject.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-gobject-native",
3
- "version": "0.64.1-next.241",
3
+ "version": "0.64.1-next.243",
4
4
  "description": "Real GObject behind @gjsify/adwaita-core's GObject subset — a GObjectEngine whose property store and signal bus are a live GObject reached through node-gi (ADR 0105)",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -25,7 +25,9 @@
25
25
  "build:test": "gjsify run build:test:node-gi",
26
26
  "build:test:node-gi": "gjsify build src/test.node-gi.mts --app node --outfile dist/test.node-gi.mjs",
27
27
  "test": "gjsify run build:gjsify && gjsify run build:test",
28
- "test:gjs-on-node": "gjsify run build:test:node-gi && node dist/test.node-gi.mjs"
28
+ "test:gjs-on-node": "gjsify run build:test:node-gi && node dist/test.node-gi.mjs",
29
+ "build:bench:node-gi": "gjsify build src/bench.node-gi.mts --app node --outfile dist/bench.node-gi.mjs",
30
+ "bench:node-gi": "gjsify run build:bench:node-gi && node dist/bench.node-gi.mjs"
29
31
  },
30
32
  "keywords": [
31
33
  "gjs",
@@ -36,13 +38,13 @@
36
38
  "adwaita"
37
39
  ],
38
40
  "dependencies": {
39
- "@gjsify/adwaita-core": "0.64.1-next.241"
41
+ "@gjsify/adwaita-core": "0.64.1-next.243"
40
42
  },
41
43
  "devDependencies": {
42
- "@gjsify/app-settings": "0.64.1-next.241",
43
- "@gjsify/cli": "0.64.1-next.241",
44
+ "@gjsify/app-settings": "0.64.1-next.243",
45
+ "@gjsify/cli": "0.64.1-next.243",
44
46
  "@gjsify/node-gi": "file:../../node-gi/node-gi",
45
- "@gjsify/unit": "0.64.1-next.241",
47
+ "@gjsify/unit": "0.64.1-next.243",
46
48
  "@types/node": "^25.9.2",
47
49
  "typescript": "^6.0.3"
48
50
  },
@@ -68,5 +70,5 @@
68
70
  "url": "https://github.com/gjsify/gjsify/issues"
69
71
  },
70
72
  "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/nativescript-bridge/gobject#readme",
71
- "gitHead": "9937e8d27d18d159e35471b6669bdca6dff0cd05"
73
+ "gitHead": "f88c7b1074d454e211094963a9f4dc7d0982bf32"
72
74
  }