perf(cockpit): a Rail or Transit snapshot read again while nothing in it changed is the one read before

getSelectedTrain, getSelectedVehicle and getCockpitSubject fill drafts and
freeze a new object only for a part whose values changed, so a view that
reads them on every frame builds nothing while the train or vehicle stands
still, and a moving one keeps its feed, stop and provenance objects. A
train's stops are copied once per list the stops provider returns, each
stop a frozen copy, instead of on every call.
This commit is contained in:
Denis Groshev committed 2026-11-12 16:58:38 +01:00
1 parent a4f321a9ef
commit 294ccc0ee8
4 files changed
+303 -141

No files matched your search

+126 -72
View File
@@ -2,12 +2,50 @@ import { pointAtS } from '../../data/railPaths.js';
import { getRegisteredRailFeed } from '../../data/railFeeds.js'; import { getRegisteredRailFeed } from '../../data/railFeeds.js';
import { displayMotion } from '../transit/movement.js'; import { displayMotion } from '../transit/movement.js';
const NO_STOPS = Object.freeze([]);
/** Each stops list's frozen copy for a snapshot's `stops` (stopsOf). */
const stopLists = new WeakMap();
/**
* A trip's stops as a snapshot hands them out: a frozen array of frozen
* copies, made once per list the stops provider returns.
*/
function stopsOf(list) {
if (!Array.isArray(list)) return NO_STOPS;
let stops = stopLists.get(list);
if (!stops) {
stops = Object.freeze(
list.map((stop) =>
stop && typeof stop === 'object' ? Object.freeze({ ...stop }) : stop,
),
);
stopLists.set(list, stops);
}
return stops;
}
/**
* `previous` when it holds each of `next`'s values, else a frozen copy of
* `next`: a part of a snapshot that did not change is the one read before.
* `next` is a draft, filled again on every read.
*/
function reuse(previous, next) {
if (!previous) return Object.freeze({ ...next });
for (const name in next)
if (!Object.is(previous[name], next[name]))
return Object.freeze({ ...next });
return previous;
}
/** /**
* Read access to one train, and a hold on it for a first-person view. * Read access to one train, and a hold on it for a first-person view.
* *
* `getSelectedTrain()` and `getCockpitSubject()` describe the selected train * `getSelectedTrain()` and `getCockpitSubject()` describe the selected train
* and the held one as a frozen snapshot, read again on each call: where it is * and the held one as a frozen snapshot, read again on each call: where it is
* along its line, how fast it moves and how that is known, and its stops. * along its line, how fast it moves and how that is known, and its stops. A
* call while nothing in it changed returns the snapshot it returned before,
* and a part that did not change (its feed, its stops) is the same object in
* the next one, so a view can read it on every frame.
* `setCockpitSubject(key)` holds a train: its marker, card and selected line * `setCockpitSubject(key)` holds a train: its marker, card and selected line
* are hidden, it stays selected (so its stops stay fresh and its feed stays * are hidden, it stays selected (so its stops stay fresh and its feed stays
* polled at any distance) and clicks and Escape no longer change the * polled at any distance) and clicks and Escape no longer change the
@@ -36,26 +74,37 @@ export function createRailCockpitSubject({
const finiteOrNull = (value) => const finiteOrNull = (value) =>
typeof value === 'number' && Number.isFinite(value) ? value : null; typeof value === 'number' && Number.isFinite(value) ? value : null;
const scratch = {}; const scratch = {};
/** Each train's last snapshot, returned again while nothing in it changed. */
const views = new WeakMap();
/** The drafts trainView fills on every read (reuse). */
const draft = {
view: {},
feed: {},
position: {},
path: {},
motion: {},
provenance: {},
};
/** The catalog's public entry for a feed, else the registry's. */ /** The catalog's public entry for a feed, else the registry's. */
function feedOf(feedId) { function feedOf(feedId, previous) {
const listed = state._catalog?.find((feed) => feed.id === feedId); const listed = state._catalog?.find((feed) => feed.id === feedId);
const feed = listed ?? getRegisteredRailFeed(feedId); const feed = listed ?? getRegisteredRailFeed(feedId);
if (!feed) return null; if (!feed) return null;
return Object.freeze({ const next = draft.feed;
id: feed.id, next.id = feed.id;
name: feed.name ?? null, next.name = feed.name ?? null;
operator: feed.operator ?? null, next.operator = feed.operator ?? null;
region: feed.region ?? null, next.region = feed.region ?? null;
positions: listed?.positions ?? feed.positions ?? 'gps', next.positions = listed?.positions ?? feed.positions ?? 'gps';
tripUpdates: next.tripUpdates =
typeof listed?.tripUpdates === 'boolean' typeof listed?.tripUpdates === 'boolean'
? listed.tripUpdates ? listed.tripUpdates
: typeof feed.tripUpdatesUrl === 'string', : typeof feed.tripUpdatesUrl === 'string';
attribution: feed.attribution ?? null, next.attribution = feed.attribution ?? null;
license: feed.license ?? null, next.license = feed.license ?? null;
licenseUrl: feed.licenseUrl ?? null, next.licenseUrl = feed.licenseUrl ?? null;
}); return reuse(previous, next);
} }
/** The route's short (else long) name in the feed's timetable, or null. */ /** The route's short (else long) name in the feed's timetable, or null. */
@@ -87,6 +136,7 @@ export function createRailCockpitSubject({
/** The train as a frozen snapshot (see the module comment), or null. */ /** The train as a frozen snapshot (see the module comment), or null. */
function trainView(entry) { function trainView(entry) {
if (!entry?.record) return null; if (!entry?.record) return null;
const previous = views.get(entry);
const record = entry.record; const record = entry.record;
const sample = entry.sample ?? null; const sample = entry.sample ?? null;
const drawn = entry.drawnHeight ?? null; const drawn = entry.drawnHeight ?? null;
@@ -95,64 +145,68 @@ export function createRailCockpitSubject({
const derived = finiteOrNull(sample?.segmentSpeedMps); const derived = finiteOrNull(sample?.segmentSpeedMps);
const trip = state._selectedKey === entry.key ? state._trip : null; const trip = state._selectedKey === entry.key ? state._trip : null;
const status = state._feedStatus.get(entry.feedId); const status = state._feedStatus.get(entry.feedId);
const latitude =
const position = draft.position;
position.latitude =
finiteOrNull(drawn?.lat) ?? finiteOrNull(sample?.lat) ?? record.lat; finiteOrNull(drawn?.lat) ?? finiteOrNull(sample?.lat) ?? record.lat;
const longitude = position.longitude =
finiteOrNull(drawn?.lon) ?? finiteOrNull(sample?.lon) ?? record.lon; finiteOrNull(drawn?.lon) ?? finiteOrNull(sample?.lon) ?? record.lon;
return Object.freeze({ position.railHeightM = Number.isFinite(drawn?.h) ? drawn.h - liftM : null;
key: entry.key, position.heightQuality = Number.isFinite(drawn?.q) ? drawn.q : 0;
feedId: entry.feedId, position.atMs = finiteOrNull(drawn?.at);
selectedAtMs:
state._selectedKey === entry.key ? state._selectedAtMs : null, let path = null;
id: String(record.id ?? ''), if (binding && binding.path) {
tripId: record.tripId || null, path = draft.path;
routeId: record.routeId || null, path.shapeIndex = binding.shapeIndex;
routeName: routeNameOf(entry), path.s = binding.drawS;
label: record.label || null, path.direction = directionOf(entry);
trainNumber: record.trainNumber || null, path.lengthM = binding.path.length;
headsign: trip?.headsign || null, path.via = binding.via;
category: entry.category ?? null, path.builtAt = state._pathPacks.get(entry.feedId)?.pack?.builtAt ?? null;
mode: entry.railMode ?? null, path.line = binding.path;
feed: feedOf(entry.feedId), }
position: Object.freeze({
latitude, const motion = draft.motion;
longitude, motion.reportedSpeedMps = reported;
railHeightM: Number.isFinite(drawn?.h) ? drawn.h - liftM : null, motion.derivedSpeedMps = derived;
heightQuality: Number.isFinite(drawn?.q) ? drawn.q : 0, motion.speedSource =
atMs: finiteOrNull(drawn?.at), reported !== null ? 'reported' : derived !== null ? 'derived' : null;
}), motion.courseDeg = finiteOrNull(entry.courseDeg);
path: motion.phase = sample?.phase ?? null;
binding && binding.path motion.word = displayMotion(entry, Date.now()).word;
? Object.freeze({ motion.reportAgeMs = finiteOrNull(sample?.latestReportAgeMs);
shapeIndex: binding.shapeIndex, motion.shownBehindMs = finiteOrNull(sample?.actualDelayMs);
s: binding.drawS, motion.feedStale = status?.stale === true;
direction: directionOf(entry),
lengthM: binding.path.length, const provenance = draft.provenance;
via: binding.via, provenance.positionSource = record.positionSource ?? 'gps';
builtAt: provenance.timestampSource = record.timestampSource ?? null;
state._pathPacks.get(entry.feedId)?.pack?.builtAt ?? null,
line: binding.path, const view = draft.view;
}) view.key = entry.key;
: null, view.feedId = entry.feedId;
motion: Object.freeze({ view.selectedAtMs =
reportedSpeedMps: reported, state._selectedKey === entry.key ? state._selectedAtMs : null;
derivedSpeedMps: derived, view.id = String(record.id ?? '');
speedSource: view.tripId = record.tripId || null;
reported !== null ? 'reported' : derived !== null ? 'derived' : null, view.routeId = record.routeId || null;
courseDeg: finiteOrNull(entry.courseDeg), view.routeName = routeNameOf(entry);
phase: sample?.phase ?? null, view.label = record.label || null;
word: displayMotion(entry, Date.now()).word, view.trainNumber = record.trainNumber || null;
reportAgeMs: finiteOrNull(sample?.latestReportAgeMs), view.headsign = trip?.headsign || null;
shownBehindMs: finiteOrNull(sample?.actualDelayMs), view.category = entry.category ?? null;
feedStale: status?.stale === true, view.mode = entry.railMode ?? null;
}), view.feed = feedOf(entry.feedId, previous?.feed);
provenance: Object.freeze({ view.position = reuse(previous?.position, position);
positionSource: record.positionSource ?? 'gps', view.path = path && reuse(previous?.path, path);
timestampSource: record.timestampSource ?? null, view.motion = reuse(previous?.motion, motion);
}), view.provenance = reuse(previous?.provenance, provenance);
stops: Object.freeze(Array.isArray(trip?.stops) ? [...trip.stops] : []), view.stops = stopsOf(trip?.stops);
match: trip?.match ?? null, view.match = trip?.match ?? null;
}); const snapshot = reuse(previous, view);
views.set(entry, snapshot);
return snapshot;
} }
const methods = { const methods = {
+57 -3
View File
@@ -4,7 +4,7 @@
// hides its marker and card, keeps it selected and ends when it goes. // hides its marker and card, keeps it selected and ends when it goes.
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { RAIL_POLL_MS, railTrainKey } from './policy.js'; import { RAIL_POLL_MS, STOP_REFRESH_MS, railTrainKey } from './policy.js';
import { import {
BOSTON, BOSTON,
railLayerHarness as harness, railLayerHarness as harness,
@@ -37,8 +37,12 @@ const PACK = Object.freeze({
}); });
/** Two trains on the line, T1 running east and T2 west, a step a poll. */ /** Two trains on the line, T1 running east and T2 west, a step a poll. */
async function onTheLine(t) { async function onTheLine(t, options = {}) {
const app = harness(t, { paths: { 'mbta-rail': PACK }, at: BOSTON }); const app = harness(t, {
paths: { 'mbta-rail': PACK },
at: BOSTON,
...options,
});
let step = 0; let step = 0;
app.feeds.set('mbta-rail', () => [ app.feeds.set('mbta-rail', () => [
train('T1', LAT, LON0 + (1000 + step * 150) / KX, { speedMps: 15 }), train('T1', LAT, LON0 + (1000 + step * 150) / KX, { speedMps: 15 }),
@@ -100,6 +104,56 @@ test('the selected train reads as a snapshot of its place, motion and sources',
); );
}); });
/** A trip's stops as a stops provider lists them. */
const stopList = (...names) =>
names.map((name, i) => ({
stopId: name,
name,
lat: LAT,
lon: LON0 + (i * 2000) / KX,
state: 'upcoming',
}));
test('a snapshot read again while nothing changed is the one read before', async (t) => {
let list = stopList('Alpha', 'Bravo', 'Charlie');
const app = await onTheLine(t, {
stops: { stopsFor: () => ({ stops: list, headsign: 'Charlie' }) },
});
app.parts().selection.selectTrain(T1);
const first = app.layer.getSelectedTrain();
assert.equal(app.layer.getSelectedTrain(), first, 'the same snapshot');
// Its stops are frozen copies of the provider's.
assert.equal(first.stops.length, 3);
assert.ok(Object.isFrozen(first.stops));
assert.ok(Object.isFrozen(first.stops[0]));
assert.notEqual(first.stops[0], list[0]);
assert.equal(first.stops[0].name, 'Alpha');
// It moves on: a new snapshot at its new place, what did not change kept.
app.run(RAIL_POLL_MS);
await app.poll();
app.run(RAIL_POLL_MS);
const moved = app.layer.getSelectedTrain();
assert.notEqual(moved, first, 'a new snapshot');
assert.ok(Object.isFrozen(moved) && Object.isFrozen(moved.path));
assert.ok(moved.path.s > first.path.s, 'at its new place');
assert.equal(moved.stops, first.stops, 'the same stops');
assert.equal(moved.feed, first.feed);
assert.equal(moved.provenance, first.provenance);
assert.equal(moved.path.line, first.path.line);
assert.equal(app.layer.getSelectedTrain(), moved);
// The provider lists other stops: new ones.
list = stopList('Delta', 'Echo');
app.run(STOP_REFRESH_MS);
const later = app.layer.getSelectedTrain();
assert.notEqual(later.stops, first.stops);
assert.deepEqual(
later.stops.map((stop) => stop.name),
['Delta', 'Echo'],
);
});
test('a held train hides its marker and card and keeps the selection', async (t) => { test('a held train hides its marker and card and keeps the selection', async (t) => {
const app = await onTheLine(t); const app = await onTheLine(t);
assert.equal(app.layer.setCockpitSubject('rail:mbta-rail:nope'), false); assert.equal(app.layer.setCockpitSubject('rail:mbta-rail:nope'), false);
+99 -66
View File
@@ -4,17 +4,33 @@ import {
} from '../../data/transitFeeds.js'; } from '../../data/transitFeeds.js';
import { displayMotion } from './movement.js'; import { displayMotion } from './movement.js';
/**
* `previous` when it holds each of `next`'s values, else a frozen copy of
* `next`: a part of a snapshot that did not change is the one read before.
* `next` is a draft, filled again on every read.
*/
function reuse(previous, next) {
if (!previous) return Object.freeze({ ...next });
for (const name in next)
if (!Object.is(previous[name], next[name]))
return Object.freeze({ ...next });
return previous;
}
/** /**
* Read access to one vehicle, and a hold on it for a first-person view. * Read access to one vehicle, and a hold on it for a first-person view.
* *
* `getSelectedVehicle()` and `getCockpitSubject()` describe the selected * `getSelectedVehicle()` and `getCockpitSubject()` describe the selected
* vehicle and the held one as a frozen snapshot, read again on each call: * vehicle and the held one as a frozen snapshot, read again on each call:
* where it is drawn, how fast it moves and how that is known. Transit has no * where it is drawn, how fast it moves and how that is known. A call while
* route shapes or stops, so a snapshot names only the vehicle's last reported * nothing in it changed returns the snapshot it returned before, and a part
* stop. `setCockpitSubject(key)` holds a vehicle: its sprite, card, trail * that did not change (its feed, its stop) is the same object in the next
* and detection box are hidden, it keeps moving when it is out of the * one, so a view can read it on every frame. Transit has no route shapes or
* camera's view, its feed stays polled, and clicks and Escape no longer * stops, so a snapshot names only the vehicle's last reported stop.
* change the selection. The hold ends with `setCockpitSubject(null)`, and on * `setCockpitSubject(key)` holds a vehicle: its sprite, card, trail and
* detection box are hidden, it keeps moving when it is out of the camera's
* view, its feed stays polled, and clicks and Escape no longer change the
* selection. The hold ends with `setCockpitSubject(null)`, and on
* its own when the vehicle is removed or the layer is turned off. * its own when the vehicle is removed or the layer is turned off.
* `focusVehicle(key)` selects a vehicle, as a click does. A snapshot's * `focusVehicle(key)` selects a vehicle, as a click does. A snapshot's
* `selectedAtMs` is when the user last selected the selected vehicle (a * `selectedAtMs` is when the user last selected the selected vehicle (a
@@ -31,84 +47,101 @@ export function createTransitCockpitSubject({ state, parts, services }) {
: 0; : 0;
const finiteOrNull = (value) => const finiteOrNull = (value) =>
typeof value === 'number' && Number.isFinite(value) ? value : null; typeof value === 'number' && Number.isFinite(value) ? value : null;
/** Each vehicle's last snapshot, returned again while nothing changed. */
const views = new WeakMap();
/** The drafts vehicleView fills on every read (reuse). */
const draft = {
view: {},
feed: {},
position: {},
motion: {},
stop: {},
provenance: {},
};
/** The feed in use, else the registry's entry for it. */ /** The feed in use, else the registry's entry for it. */
function feedEntry(feedId) { function feedEntry(feedId) {
return state._activeFeeds.get(feedId) || getRegisteredTransitFeed(feedId); return state._activeFeeds.get(feedId) || getRegisteredTransitFeed(feedId);
} }
function feedOf(feed) { function feedOf(feed, previous) {
if (!feed) return null; if (!feed) return null;
return Object.freeze({ const next = draft.feed;
id: feed.id, next.id = feed.id;
name: feed.name ?? null, next.name = feed.name ?? null;
operator: feed.operator ?? null, next.operator = feed.operator ?? null;
region: feed.region ?? null, next.region = feed.region ?? null;
attribution: feed.attribution ?? null, next.attribution = feed.attribution ?? null;
license: feed.license ?? null, next.license = feed.license ?? null;
licenseUrl: feed.licenseUrl ?? null, next.licenseUrl = feed.licenseUrl ?? null;
}); return reuse(previous, next);
} }
/** The vehicle as a frozen snapshot (see the module comment), or null. */ /** The vehicle as a frozen snapshot (see the module comment), or null. */
function vehicleView(entry) { function vehicleView(entry) {
if (!entry?.record) return null; if (!entry?.record) return null;
const previous = views.get(entry);
const record = entry.record; const record = entry.record;
const sample = entry.sample ?? null; const sample = entry.sample ?? null;
const feed = feedEntry(entry.feedId); const feed = feedEntry(entry.feedId);
const reported = finiteOrNull(record.speedMps); const reported = finiteOrNull(record.speedMps);
const derived = finiteOrNull(sample?.segmentSpeedMps); const derived = finiteOrNull(sample?.segmentSpeedMps);
const status = state._feedStatus.get(entry.feedId); const status = state._feedStatus.get(entry.feedId);
const latitude = finiteOrNull(sample?.lat) ?? record.lat;
const longitude = finiteOrNull(sample?.lon) ?? record.lon; const position = draft.position;
return Object.freeze({ position.latitude = finiteOrNull(sample?.lat) ?? record.lat;
key: entry.key, position.longitude = finiteOrNull(sample?.lon) ?? record.lon;
feedId: entry.feedId, position.groundHeightM = Number.isFinite(entry.heightM)
selectedAtMs: ? entry.heightM - liftM
state._selectedKey === entry.key ? state._selectedAtMs : null, : null;
id: String(record.id ?? ''), position.atMs = finiteOrNull(sample?.displayT);
routeId: record.routeId || null,
routeLabel: const motion = draft.motion;
(feed && record.routeId motion.reportedSpeedMps = reported;
? transitRouteLabel(feed, record.routeId) motion.derivedSpeedMps = derived;
: null) || null, motion.speedSource =
tripId: record.tripId || null, reported !== null ? 'reported' : derived !== null ? 'derived' : null;
directionId: record.directionId ?? null, motion.courseDeg = finiteOrNull(entry.courseDeg);
label: record.label || null, motion.reportedBearingDeg = finiteOrNull(record.bearing);
mode: entry.mode ?? null, motion.segmentCourseDeg = finiteOrNull(sample?.segmentCourseDeg);
modeInferred: entry.modeInferred === true, motion.phase = sample?.phase ?? null;
feed: feedOf(feed), motion.word = displayMotion(entry, Date.now()).word;
position: Object.freeze({ motion.reportAgeMs = finiteOrNull(sample?.latestReportAgeMs);
latitude, motion.shownBehindMs = finiteOrNull(sample?.actualDelayMs);
longitude, motion.feedStale = status?.stale === true;
groundHeightM: Number.isFinite(entry.heightM)
? entry.heightM - liftM const stop = draft.stop;
: null, stop.stopId = record.stopId || null;
atMs: finiteOrNull(sample?.displayT), stop.status = record.status || null;
}),
motion: Object.freeze({ const provenance = draft.provenance;
reportedSpeedMps: reported, provenance.positionSource = 'gps';
derivedSpeedMps: derived, provenance.timestampSource = record.timestampSource ?? null;
speedSource:
reported !== null ? 'reported' : derived !== null ? 'derived' : null, const view = draft.view;
courseDeg: finiteOrNull(entry.courseDeg), view.key = entry.key;
reportedBearingDeg: finiteOrNull(record.bearing), view.feedId = entry.feedId;
segmentCourseDeg: finiteOrNull(sample?.segmentCourseDeg), view.selectedAtMs =
phase: sample?.phase ?? null, state._selectedKey === entry.key ? state._selectedAtMs : null;
word: displayMotion(entry, Date.now()).word, view.id = String(record.id ?? '');
reportAgeMs: finiteOrNull(sample?.latestReportAgeMs), view.routeId = record.routeId || null;
shownBehindMs: finiteOrNull(sample?.actualDelayMs), view.routeLabel =
feedStale: status?.stale === true, (feed && record.routeId
}), ? transitRouteLabel(feed, record.routeId)
stop: Object.freeze({ : null) || null;
stopId: record.stopId || null, view.tripId = record.tripId || null;
status: record.status || null, view.directionId = record.directionId ?? null;
}), view.label = record.label || null;
provenance: Object.freeze({ view.mode = entry.mode ?? null;
positionSource: 'gps', view.modeInferred = entry.modeInferred === true;
timestampSource: record.timestampSource ?? null, view.feed = feedOf(feed, previous?.feed);
}), view.position = reuse(previous?.position, position);
}); view.motion = reuse(previous?.motion, motion);
view.stop = reuse(previous?.stop, stop);
view.provenance = reuse(previous?.provenance, provenance);
const snapshot = reuse(previous, view);
views.set(entry, snapshot);
return snapshot;
} }
/** Show or hide a vehicle's sprite and trail for the hold. */ /** Show or hide a vehicle's sprite and trail for the hold. */
+21
View File
@@ -4196,6 +4196,27 @@ test('the selected vehicle reads as a snapshot of its place, motion and source',
assert.ok(Number.isFinite(view.position.latitude)); assert.ok(Number.isFinite(view.position.latitude));
}); });
test('a vehicle read again while nothing changed is the snapshot read before', async (t) => {
const app = harness(t);
const key = await busRunningNorth(app);
app.layer._transitPartsForTest().selection.selectVehicle(key);
const first = app.layer.getSelectedVehicle();
assert.equal(app.layer.getSelectedVehicle(), first, 'the same snapshot');
// It moves on: a new snapshot at its new place, what did not change kept.
app.run(TRANSIT_POLL_MS);
await app.layer.update();
app.settle();
app.run(TRANSIT_POLL_MS);
const moved = app.layer.getSelectedVehicle();
assert.notEqual(moved, first, 'a new snapshot');
assert.ok(Object.isFrozen(moved) && Object.isFrozen(moved.position));
assert.ok(moved.position.latitude > first.position.latitude, 'moved');
assert.equal(moved.feed, first.feed);
assert.equal(moved.stop, first.stop);
assert.equal(moved.provenance, first.provenance);
assert.equal(app.layer.getSelectedVehicle(), moved);
});
test('selectedAtMs is the user’s selection time; a hold and a deselection leave none', async (t) => { test('selectedAtMs is the user’s selection time; a hold and a deselection leave none', async (t) => {
const app = harness(t); const app = harness(t);
const key = await busRunningNorth(app); const key = await busRunningNorth(app);