From cdab982149ccf2f1aea3b8f3aa7db9cd19c08278 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:39:41 +0000 Subject: [PATCH] perf: Add O(1) caching for station lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces inefficient O(N*M) lookups inside `TripsPage.tsx`, `WalkTripEditor.tsx`, and `RailRound.jsx` with an O(1) cache. 💡 What: Implemented a cached Map inside `src/core/railwayRouting.ts` (`stationIdIndexCache`) to index stations by ID. 🎯 Why: Iterating over `Object.values(railwayData)` to find stations by ID caused large temporary allocations and blocked the UI thread, especially when rendering walk trips. 📊 Impact: Reduces time complexity of lookup from O(N * M) to O(1), significantly accelerating render performance for trip pages. 🔬 Measurement: Verified caching correctness and reference-equality invalidation via manual test files, verified components compilation via TS checker. Co-authored-by: OsakaLOOP <68284076+OsakaLOOP@users.noreply.github.com> --- .jules/bolt.md | 3 ++ src/RailRound.jsx | 13 +++++---- src/components/modals/WalkTripEditor.tsx | 21 ++++++-------- src/core/railwayRouting.ts | 35 +++++++++++++++++++----- src/pages/TripsPage.tsx | 11 ++++---- 5 files changed, 52 insertions(+), 31 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index ea6a1af..bc13bd3 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -5,3 +5,6 @@ ## 2024-04-15 - [Avoid O(N log N) Sorting on Massive Geographical Collections] **Learning:** In spatial queries like `findNearbyStations` where we scan `railwayData` containing thousands of stations to find the top K nearest points, allocating all elements to an array and running `Array.prototype.sort()` results in massive temporary object allocation and $O(N \log N)$ execution time (taking ~8.5ms in benchmarks). **Action:** Replace full array sorts with a bounded Top-K array using a simple $O(K)$ insertion sort during the $O(N)$ iteration phase. This brings the time complexity effectively down to $O(N)$, speeding up operations by ~36x (taking ~0.24ms). Remember to apply a final sort if total elements found are less than $K$. +## 2024-05-18 - [O(1) Dictionary Lookup for Arrays] +**Learning:** In highly nested arrays and components, running `Object.values(railwayData).forEach()` multiple times to scan through `line.stations.find()` just to lookup a single ID results in $O(N \times M)$ overhead, blocking UI threads. +**Action:** Implemented a one-time cached `Map` in `src/core/railwayRouting.ts` (`stationIdIndexCache`) to index stations by their IDs for O(1) instantaneous lookups. The cache safely invalidates by comparing the reference identity of the underlying global immutable data. diff --git a/src/RailRound.jsx b/src/RailRound.jsx index ce38ed8..0dc46d2 100644 --- a/src/RailRound.jsx +++ b/src/RailRound.jsx @@ -34,6 +34,7 @@ const { meta } = changelog; import { useStore } from './store'; import toast from 'react-hot-toast'; import { ESLint } from 'eslint'; +import { getStationById } from './core/railwayRouting'; const CURRENT_VERSION = meta["currentVersion"]; const LAST_MODIFIED = manifest.lastModified @@ -1267,12 +1268,10 @@ const RecordsView = ({ trips, railwayData, setTrips, onEdit, onDelete, onAdd, se if (isWalk) { let startName = t.fromId || ''; let endName = t.toId || ''; - Object.values(railwayData).forEach(line => { - const s = line.stations.find(st => st.id === t.fromId); + const s = getStationById(railwayData, t.fromId); if (s) startName = s.name_ja; - const e = line.stations.find(st => st.id === t.toId); + const e = getStationById(railwayData, t.toId); if (e) endName = e.name_ja; - }); const isTree = t.walkType === 'tree'; const cls = { @@ -1462,9 +1461,11 @@ const StatsView = ({ trips, railwayData, geoData, user, userProfile, segmentGeom let count = 0; if (railwayData) { const uniqueStations = new Set(); - Object.values(railwayData).forEach(line => { + for (const lineKey in railwayData) { + if (!Object.prototype.hasOwnProperty.call(railwayData, lineKey)) continue; + const line = railwayData[lineKey]; if (line.stations) line.stations.forEach(s => uniqueStations.add(s.id)); - }); + } count = uniqueStations.size; } return count; diff --git a/src/components/modals/WalkTripEditor.tsx b/src/components/modals/WalkTripEditor.tsx index 1d3cf52..bc460e6 100644 --- a/src/components/modals/WalkTripEditor.tsx +++ b/src/components/modals/WalkTripEditor.tsx @@ -6,6 +6,7 @@ import { useUserData } from '../../hooks/useUserData'; import * as turf from '@turf/turf'; import { useTranslation } from 'react-i18next'; import { showAlert, showConfirm } from '../../utils/alerts'; +import { getStationById } from '../../core/railwayRouting'; export const WalkTripEditor: React.FC = () => { const { t } = useTranslation(); @@ -82,12 +83,10 @@ export const WalkTripEditor: React.FC = () => { // Find coordinates for the Bezier curve let startCoords = null; let endCoords = null; - Object.values(railwayData).forEach(line => { - const s = line.stations.find(st => st.id === form.fromId); - if (s) startCoords = [s.lng, s.lat]; - const e = line.stations.find(st => st.id === form.toId); - if (e) endCoords = [e.lng, e.lat]; - }); + const s = getStationById(railwayData, form.fromId); + if (s) startCoords = [s.lng, s.lat]; + const e = getStationById(railwayData, form.toId); + if (e) endCoords = [e.lng, e.lat]; if (startCoords && endCoords) { walkPath = generateBezierPath(startCoords as [number, number], endCoords as [number, number]); @@ -130,12 +129,10 @@ export const WalkTripEditor: React.FC = () => { // Resolving station names for read-only display let startName = t('walk.unknownStart', "未知起点"); let endName = t('walk.unknownEnd', "未知终点"); - Object.values(railwayData).forEach(line => { - const s = line.stations.find(st => st.id === form.fromId); - if (s) startName = s.name_ja; - const e = line.stations.find(st => st.id === form.toId); - if (e) endName = e.name_ja; - }); + const s = getStationById(railwayData, form.fromId); + if (s) startName = s.name_ja; + const e = getStationById(railwayData, form.toId); + if (e) endName = e.name_ja; const isTree = form.walkType === 'tree'; diff --git a/src/core/railwayRouting.ts b/src/core/railwayRouting.ts index 3db3cbd..06f52c1 100644 --- a/src/core/railwayRouting.ts +++ b/src/core/railwayRouting.ts @@ -3,6 +3,7 @@ import { calcDist } from '../core/tripCalculator'; // Ensure calcDist is exporte // 预构建的换乘站索引缓存 let stationNameIndexCache: Map | null = null; +let stationIdIndexCache: Map | null = null; let lastRailwayDataRef: RailwayMap | null = null; export const isCompanyCompatible = (meta1: CompanyMeta | undefined, meta2: CompanyMeta | undefined) => { @@ -13,25 +14,45 @@ export const isCompanyCompatible = (meta1: CompanyMeta | undefined, meta2: Compa }; export const buildStationIndex = (railwayData: RailwayMap) => { - if (stationNameIndexCache && lastRailwayDataRef === railwayData) { + if (stationNameIndexCache && stationIdIndexCache && lastRailwayDataRef === railwayData) { return stationNameIndexCache; } - const index = new Map(); + const nameIndex = new Map(); + const idIndex = new Map(); + for (const lineKey in railwayData) { if (!Object.prototype.hasOwnProperty.call(railwayData, lineKey)) continue; const line = railwayData[lineKey]; line.stations.forEach((st, idx) => { - if (!index.has(st.name_ja)) { - index.set(st.name_ja, []); + if (!nameIndex.has(st.name_ja)) { + nameIndex.set(st.name_ja, []); + } + nameIndex.get(st.name_ja)!.push({ lineKey, stationIndex: idx }); + + if (st.id) { + idIndex.set(st.id, { lineKey, stationIndex: idx }); } - index.get(st.name_ja)!.push({ lineKey, stationIndex: idx }); }); } - stationNameIndexCache = index; + stationNameIndexCache = nameIndex; + stationIdIndexCache = idIndex; lastRailwayDataRef = railwayData; - return index; + return nameIndex; +}; + +export const getStationById = (railwayData: RailwayMap, stationId: string): Station | undefined => { + if (!stationId) return undefined; + + // Ensure caches are built + buildStationIndex(railwayData); + + if (stationIdIndexCache && stationIdIndexCache.has(stationId)) { + const info = stationIdIndexCache.get(stationId)!; + return railwayData[info.lineKey]?.stations[info.stationIndex]; + } + return undefined; }; export const getTransferableLines = (station: Station | undefined, currentLineKey: string, railwayData: RailwayMap, strictMode = true) => { diff --git a/src/pages/TripsPage.tsx b/src/pages/TripsPage.tsx index f200f4c..5ae832d 100644 --- a/src/pages/TripsPage.tsx +++ b/src/pages/TripsPage.tsx @@ -172,12 +172,10 @@ export const TripsPage: React.FC = () => { if (isWalk) { let startName = trip.fromId || ''; let endName = trip.toId || ''; - Object.values(railwayData).forEach(line => { - const s = line.stations.find(st => st.id === trip.fromId); - if (s) startName = s.name_ja; - const e = line.stations.find(st => st.id === trip.toId); - if (e) endName = e.name_ja; - }); + const s = getStationById(railwayData, trip.fromId); + if (s) startName = s.name_ja; + const e = getStationById(railwayData, trip.toId); + if (e) endName = e.name_ja; const isTree = trip.walkType === 'tree'; const cls = { @@ -288,6 +286,7 @@ export const TripsPage: React.FC = () => { }; import { ArrowUp, ArrowDown } from 'lucide-react'; +import { getStationById } from '../core/railwayRouting'; export const FloatingActionButtons: React.FC<{ fileInputRef: React.RefObject,