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 { 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.
*
* `getSelectedTrain()` and `getCockpitSubject()` describe the selected train
* 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
* 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
@@ -36,26 +74,37 @@ export function createRailCockpitSubject({
const finiteOrNull = (value) =>
typeof value === 'number' && Number.isFinite(value) ? value : null;
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. */
function feedOf(feedId) {
function feedOf(feedId, previous) {
const listed = state._catalog?.find((feed) => feed.id === feedId);
const feed = listed ?? getRegisteredRailFeed(feedId);
if (!feed) return null;
return Object.freeze({
id: feed.id,
name: feed.name ?? null,
operator: feed.operator ?? null,
region: feed.region ?? null,
positions: listed?.positions ?? feed.positions ?? 'gps',
tripUpdates:
typeof listed?.tripUpdates === 'boolean'
? listed.tripUpdates
: typeof feed.tripUpdatesUrl === 'string',
attribution: feed.attribution ?? null,
license: feed.license ?? null,
licenseUrl: feed.licenseUrl ?? null,
});
const next = draft.feed;
next.id = feed.id;
next.name = feed.name ?? null;
next.operator = feed.operator ?? null;
next.region = feed.region ?? null;
next.positions = listed?.positions ?? feed.positions ?? 'gps';
next.tripUpdates =
typeof listed?.tripUpdates === 'boolean'
? listed.tripUpdates
: typeof feed.tripUpdatesUrl === 'string';
next.attribution = feed.attribution ?? null;
next.license = feed.license ?? null;
next.licenseUrl = feed.licenseUrl ?? null;
return reuse(previous, next);
}
/** 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. */
function trainView(entry) {
if (!entry?.record) return null;
const previous = views.get(entry);
const record = entry.record;
const sample = entry.sample ?? null;
const drawn = entry.drawnHeight ?? null;
@@ -95,64 +145,68 @@ export function createRailCockpitSubject({
const derived = finiteOrNull(sample?.segmentSpeedMps);
const trip = state._selectedKey === entry.key ? state._trip : null;
const status = state._feedStatus.get(entry.feedId);
const latitude =
const position = draft.position;
position.latitude =
finiteOrNull(drawn?.lat) ?? finiteOrNull(sample?.lat) ?? record.lat;
const longitude =
position.longitude =
finiteOrNull(drawn?.lon) ?? finiteOrNull(sample?.lon) ?? record.lon;
return Object.freeze({
key: entry.key,
feedId: entry.feedId,
selectedAtMs:
state._selectedKey === entry.key ? state._selectedAtMs : null,
id: String(record.id ?? ''),
tripId: record.tripId || null,
routeId: record.routeId || null,
routeName: routeNameOf(entry),
label: record.label || null,
trainNumber: record.trainNumber || null,
headsign: trip?.headsign || null,
category: entry.category ?? null,
mode: entry.railMode ?? null,
feed: feedOf(entry.feedId),
position: Object.freeze({
latitude,
longitude,
railHeightM: Number.isFinite(drawn?.h) ? drawn.h - liftM : null,
heightQuality: Number.isFinite(drawn?.q) ? drawn.q : 0,
atMs: finiteOrNull(drawn?.at),
}),
path:
binding && binding.path
? Object.freeze({
shapeIndex: binding.shapeIndex,
s: binding.drawS,
direction: directionOf(entry),
lengthM: binding.path.length,
via: binding.via,
builtAt:
state._pathPacks.get(entry.feedId)?.pack?.builtAt ?? null,
line: binding.path,
})
: null,
motion: Object.freeze({
reportedSpeedMps: reported,
derivedSpeedMps: derived,
speedSource:
reported !== null ? 'reported' : derived !== null ? 'derived' : null,
courseDeg: finiteOrNull(entry.courseDeg),
phase: sample?.phase ?? null,
word: displayMotion(entry, Date.now()).word,
reportAgeMs: finiteOrNull(sample?.latestReportAgeMs),
shownBehindMs: finiteOrNull(sample?.actualDelayMs),
feedStale: status?.stale === true,
}),
provenance: Object.freeze({
positionSource: record.positionSource ?? 'gps',
timestampSource: record.timestampSource ?? null,
}),
stops: Object.freeze(Array.isArray(trip?.stops) ? [...trip.stops] : []),
match: trip?.match ?? null,
});
position.railHeightM = Number.isFinite(drawn?.h) ? drawn.h - liftM : null;
position.heightQuality = Number.isFinite(drawn?.q) ? drawn.q : 0;
position.atMs = finiteOrNull(drawn?.at);
let path = null;
if (binding && binding.path) {
path = draft.path;
path.shapeIndex = binding.shapeIndex;
path.s = binding.drawS;
path.direction = directionOf(entry);
path.lengthM = binding.path.length;
path.via = binding.via;
path.builtAt = state._pathPacks.get(entry.feedId)?.pack?.builtAt ?? null;
path.line = binding.path;
}
const motion = draft.motion;
motion.reportedSpeedMps = reported;
motion.derivedSpeedMps = derived;
motion.speedSource =
reported !== null ? 'reported' : derived !== null ? 'derived' : null;
motion.courseDeg = finiteOrNull(entry.courseDeg);
motion.phase = sample?.phase ?? null;
motion.word = displayMotion(entry, Date.now()).word;
motion.reportAgeMs = finiteOrNull(sample?.latestReportAgeMs);
motion.shownBehindMs = finiteOrNull(sample?.actualDelayMs);
motion.feedStale = status?.stale === true;
const provenance = draft.provenance;
provenance.positionSource = record.positionSource ?? 'gps';
provenance.timestampSource = record.timestampSource ?? null;
const view = draft.view;
view.key = entry.key;
view.feedId = entry.feedId;
view.selectedAtMs =
state._selectedKey === entry.key ? state._selectedAtMs : null;
view.id = String(record.id ?? '');
view.tripId = record.tripId || null;
view.routeId = record.routeId || null;
view.routeName = routeNameOf(entry);
view.label = record.label || null;
view.trainNumber = record.trainNumber || null;
view.headsign = trip?.headsign || null;
view.category = entry.category ?? null;
view.mode = entry.railMode ?? null;
view.feed = feedOf(entry.feedId, previous?.feed);
view.position = reuse(previous?.position, position);
view.path = path && reuse(previous?.path, path);
view.motion = reuse(previous?.motion, motion);
view.provenance = reuse(previous?.provenance, provenance);
view.stops = stopsOf(trip?.stops);
view.match = trip?.match ?? null;
const snapshot = reuse(previous, view);
views.set(entry, snapshot);
return snapshot;
}
const methods = {
+57 -3
View File
@@ -4,7 +4,7 @@
// hides its marker and card, keeps it selected and ends when it goes.
import test from 'node:test';
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 {
BOSTON,
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. */
async function onTheLine(t) {
const app = harness(t, { paths: { 'mbta-rail': PACK }, at: BOSTON });
async function onTheLine(t, options = {}) {
const app = harness(t, {
paths: { 'mbta-rail': PACK },
at: BOSTON,
...options,
});
let step = 0;
app.feeds.set('mbta-rail', () => [
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) => {
const app = await onTheLine(t);
assert.equal(app.layer.setCockpitSubject('rail:mbta-rail:nope'), false);
+99 -66
View File
@@ -4,17 +4,33 @@ import {
} from '../../data/transitFeeds.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.
*
* `getSelectedVehicle()` and `getCockpitSubject()` describe the selected
* 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
* route shapes or stops, so a snapshot names only the vehicle's last reported
* stop. `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
* where it is drawn, how fast it moves and how that is known. A call while
* nothing in it changed returns the snapshot it returned before, and a part
* that did not change (its feed, its stop) is the same object in the next
* one, so a view can read it on every frame. Transit has no route shapes or
* stops, so a snapshot names only the vehicle's last reported stop.
* `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.
* `focusVehicle(key)` selects a vehicle, as a click does. A snapshot's
* `selectedAtMs` is when the user last selected the selected vehicle (a
@@ -31,84 +47,101 @@ export function createTransitCockpitSubject({ state, parts, services }) {
: 0;
const finiteOrNull = (value) =>
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. */
function feedEntry(feedId) {
return state._activeFeeds.get(feedId) || getRegisteredTransitFeed(feedId);
}
function feedOf(feed) {
function feedOf(feed, previous) {
if (!feed) return null;
return Object.freeze({
id: feed.id,
name: feed.name ?? null,
operator: feed.operator ?? null,
region: feed.region ?? null,
attribution: feed.attribution ?? null,
license: feed.license ?? null,
licenseUrl: feed.licenseUrl ?? null,
});
const next = draft.feed;
next.id = feed.id;
next.name = feed.name ?? null;
next.operator = feed.operator ?? null;
next.region = feed.region ?? null;
next.attribution = feed.attribution ?? null;
next.license = feed.license ?? null;
next.licenseUrl = feed.licenseUrl ?? null;
return reuse(previous, next);
}
/** The vehicle as a frozen snapshot (see the module comment), or null. */
function vehicleView(entry) {
if (!entry?.record) return null;
const previous = views.get(entry);
const record = entry.record;
const sample = entry.sample ?? null;
const feed = feedEntry(entry.feedId);
const reported = finiteOrNull(record.speedMps);
const derived = finiteOrNull(sample?.segmentSpeedMps);
const status = state._feedStatus.get(entry.feedId);
const latitude = finiteOrNull(sample?.lat) ?? record.lat;
const longitude = finiteOrNull(sample?.lon) ?? record.lon;
return Object.freeze({
key: entry.key,
feedId: entry.feedId,
selectedAtMs:
state._selectedKey === entry.key ? state._selectedAtMs : null,
id: String(record.id ?? ''),
routeId: record.routeId || null,
routeLabel:
(feed && record.routeId
? transitRouteLabel(feed, record.routeId)
: null) || null,
tripId: record.tripId || null,
directionId: record.directionId ?? null,
label: record.label || null,
mode: entry.mode ?? null,
modeInferred: entry.modeInferred === true,
feed: feedOf(feed),
position: Object.freeze({
latitude,
longitude,
groundHeightM: Number.isFinite(entry.heightM)
? entry.heightM - liftM
: null,
atMs: finiteOrNull(sample?.displayT),
}),
motion: Object.freeze({
reportedSpeedMps: reported,
derivedSpeedMps: derived,
speedSource:
reported !== null ? 'reported' : derived !== null ? 'derived' : null,
courseDeg: finiteOrNull(entry.courseDeg),
reportedBearingDeg: finiteOrNull(record.bearing),
segmentCourseDeg: finiteOrNull(sample?.segmentCourseDeg),
phase: sample?.phase ?? null,
word: displayMotion(entry, Date.now()).word,
reportAgeMs: finiteOrNull(sample?.latestReportAgeMs),
shownBehindMs: finiteOrNull(sample?.actualDelayMs),
feedStale: status?.stale === true,
}),
stop: Object.freeze({
stopId: record.stopId || null,
status: record.status || null,
}),
provenance: Object.freeze({
positionSource: 'gps',
timestampSource: record.timestampSource ?? null,
}),
});
const position = draft.position;
position.latitude = finiteOrNull(sample?.lat) ?? record.lat;
position.longitude = finiteOrNull(sample?.lon) ?? record.lon;
position.groundHeightM = Number.isFinite(entry.heightM)
? entry.heightM - liftM
: null;
position.atMs = finiteOrNull(sample?.displayT);
const motion = draft.motion;
motion.reportedSpeedMps = reported;
motion.derivedSpeedMps = derived;
motion.speedSource =
reported !== null ? 'reported' : derived !== null ? 'derived' : null;
motion.courseDeg = finiteOrNull(entry.courseDeg);
motion.reportedBearingDeg = finiteOrNull(record.bearing);
motion.segmentCourseDeg = finiteOrNull(sample?.segmentCourseDeg);
motion.phase = sample?.phase ?? null;
motion.word = displayMotion(entry, Date.now()).word;
motion.reportAgeMs = finiteOrNull(sample?.latestReportAgeMs);
motion.shownBehindMs = finiteOrNull(sample?.actualDelayMs);
motion.feedStale = status?.stale === true;
const stop = draft.stop;
stop.stopId = record.stopId || null;
stop.status = record.status || null;
const provenance = draft.provenance;
provenance.positionSource = 'gps';
provenance.timestampSource = record.timestampSource ?? null;
const view = draft.view;
view.key = entry.key;
view.feedId = entry.feedId;
view.selectedAtMs =
state._selectedKey === entry.key ? state._selectedAtMs : null;
view.id = String(record.id ?? '');
view.routeId = record.routeId || null;
view.routeLabel =
(feed && record.routeId
? transitRouteLabel(feed, record.routeId)
: null) || null;
view.tripId = record.tripId || null;
view.directionId = record.directionId ?? null;
view.label = record.label || null;
view.mode = entry.mode ?? null;
view.modeInferred = entry.modeInferred === true;
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. */
+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));
});
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) => {
const app = harness(t);
const key = await busRunningNorth(app);