From 18faa6d0b2c1c45edcbf9f0ca42baf9d16592996 Mon Sep 17 00:00:00 2001 From: Senrokai Date: Tue, 29 Sep 2026 23:30:53 +0200 Subject: [PATCH] Classify a star for the rows that list it, not every star each time the index is built MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The search index and the route index gave every one of the 455 571 stars a subtitle up front through spectralClassification, which filtered the dwarf table afresh on each call; the star field's tints read the same table once per BP−RP star. Both indices now carry the star itself and classify it only for the rows shown (entrySubtitle): eight search results, a few route options. The two filtered columns of the table are built once. Node, over the shipped catalogue, five runs: classifying every star 121-167 ms before, 56-62 after hoisting the table alone; reading the sequence at every BP−RP colour 187-216 ms, now 111-127. In the app on :4302, two cold loads each, the long task when the Search tab opens was 264-380 ms (median 280) before and 164-276 ms (median 171) after; the longest boot task 863 and 875 ms before, 654 and 741 after. Tests: the search spec checks its index holds no classification and the row still reads "Star · ~M8"; the scene spec now reads the route options, and checks its index holds none either. Controls: classifying every star in the search index, doing so in the route index, and route options printing the index's subtitle each fail the named test. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../galaxy-system-scene.component.spec.ts | 17 ++++++++++++++--- .../galaxy-system-scene.component.ts | 7 ++++--- src/app/features/search/search-ranking.ts | 13 +++++++++++++ .../features/search/search.component.spec.ts | 3 +++ src/app/features/search/search.component.ts | 7 +++---- src/app/shared/astro/spectral.ts | 11 +++++++++-- 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts index b46708b..27dea69 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts @@ -964,9 +964,20 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => { }); it('offers a star for a route by its classification, and a star with none by its name alone', () => { - const component = fixture.componentInstance as unknown as { starSearchIndex(): { entry: { starId?: number; subtitle: string } }[]; currentStarOption(): { subtitle: string } | null }; - expect(component.starSearchIndex().find(({ entry }) => entry.starId === LENS.id)?.entry.subtitle).toBe(''); - expect(component.starSearchIndex().find(({ entry }) => entry.starId === PROXIMA.id)?.entry.subtitle).toBe('M5V'); + const component = fixture.componentInstance as unknown as { + starSearchIndex(): { entry: { starId?: number; subtitle: string } }[]; + routeOptions(): { id: number; subtitle: string }[]; + onRouteQuery(query: string): void; + currentStarOption(): { subtitle: string } | null; + }; + const optionFor = (query: string, id: number): string | undefined => { + component.onRouteQuery(query); + return component.routeOptions().find((option) => option.id === id)?.subtitle; + }; + expect(optionFor('KMT-2016', LENS.id)).toBe(''); + expect(optionFor('Proxima', PROXIMA.id)).toBe('M5V'); + // Classified for the options shown only: the index holds none, where all 455 571 stars cost 140-230 ms at boot. + expect(component.starSearchIndex().every(({ entry }) => entry.subtitle === '')).toBe(true); navigationStore.selectStar(LENS.id); expect(component.currentStarOption()?.subtitle).toBe(''); navigationStore.selectStar(PROXIMA.id); diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.ts index d814e44..1d95359 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -66,7 +66,7 @@ import { HudReadout, } from '../hud/hud-dock.component'; import { RouteRequest, RouteResult, RouteStarOption } from '../hud/routes-panel.component'; -import { buildSearchIndex, IndexedSearchEntry, rankSearchResults } from '../search/search-ranking'; +import { buildSearchIndex, entrySubtitle, IndexedSearchEntry, rankSearchResults } from '../search/search-ranking'; import { StarmapHudComponent } from './starmap-hud.component'; import { SystemObjectCardComponent } from './system-object-card.component'; import { RoutingClient } from './routing-client'; @@ -526,7 +526,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { return rankSearchResults(index, query, ROUTE_OPTION_COUNT).flatMap((entry) => entry.starId === undefined ? [] - : [{ id: entry.starId, name: entry.name, subtitle: entry.subtitle }], + : [{ id: entry.starId, name: entry.name, subtitle: entrySubtitle(entry) }], ); }); private readonly routeQuery = signal(''); @@ -733,7 +733,8 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { stars.map((star) => ({ kind: 'star' as const, name: star.name, - subtitle: spectralClassification(star), + subtitle: '', + star, starId: star.id, })), ), diff --git a/src/app/features/search/search-ranking.ts b/src/app/features/search/search-ranking.ts index 45fce77..d5f207c 100644 --- a/src/app/features/search/search-ranking.ts +++ b/src/app/features/search/search-ranking.ts @@ -1,3 +1,5 @@ +import { spectralClassification } from '../../shared/astro/spectral'; + export type SearchResultKind = 'star' | 'body' | 'exoplanet'; export interface SearchEntry { @@ -6,10 +8,21 @@ export interface SearchEntry { subtitle: string; /** HYG star id, for `kind: 'star'` results. */ starId?: number; + /** + * The star behind a `kind: 'star'` entry, classified by {@link entrySubtitle} for the rows shown + * only. Classified up front, all 455 571 took 140-230 ms of the main thread at boot, for the + * route index, and again each time the search tab was opened. + */ + star?: Parameters[0]; /** `bodies.json`/`exoplanets.json` id, for `kind: 'body' | 'exoplanet'` results. */ bodyId?: string; } +/** The line an entry is listed with: its subtitle, or a star's classification. */ +export function entrySubtitle(entry: SearchEntry): string { + return entry.star ? spectralClassification(entry.star) : entry.subtitle; +} + /** * How well a name matches, best first. The gaps are what matter: any exact match outranks every * prefix match, and so on, so a better kind of match can never be crowded out by a worse one. diff --git a/src/app/features/search/search.component.spec.ts b/src/app/features/search/search.component.spec.ts index d1b65f2..c5aaef8 100644 --- a/src/app/features/search/search.component.spec.ts +++ b/src/app/features/search/search.component.spec.ts @@ -118,6 +118,9 @@ describe('SearchComponent', () => { expect(kindLine()).toBe('Star · ~M8'); await type('KMT-2016-BLG-1107L'); expect(kindLine()).toBe('Star'); + // Classified for the rows shown only: over all 455 571 stars, each opening of the tab spent 140-230 ms on it. + const index = (fixture.componentInstance as unknown as { index(): { entry: { kind: string; subtitle: string } }[] }).index(); + expect(index.filter(({ entry }) => entry.kind === 'star').every(({ entry }) => entry.subtitle === '')).toBe(true); }); it('shows nothing for a query that matches nothing', async () => { diff --git a/src/app/features/search/search.component.ts b/src/app/features/search/search.component.ts index 5b72bc5..9bd9486 100644 --- a/src/app/features/search/search.component.ts +++ b/src/app/features/search/search.component.ts @@ -2,10 +2,9 @@ import { Component, computed, ElementRef, output, signal, viewChild } from '@ang import { Router } from '@angular/router'; import { DataLoaderService } from '../../core/data/data-loader.service'; -import { spectralClassification } from '../../shared/astro/spectral'; import { NavigationStore } from '../../shared/state/navigation.store'; import { ReticleIconComponent } from '../../shared/ui/reticle-icon.component'; -import { buildSearchIndex, IndexedSearchEntry, rankSearchResults, SearchEntry, SearchResultKind } from './search-ranking'; +import { buildSearchIndex, entrySubtitle, IndexedSearchEntry, rankSearchResults, SearchEntry, SearchResultKind } from './search-ranking'; const MAX_RESULTS = 8; const MIN_QUERY_LENGTH = 2; @@ -108,7 +107,7 @@ export class SearchComponent { }); readonly matchTotal = computed(() => this.matches().length); - readonly results = computed(() => this.matches().slice(0, MAX_RESULTS)); + readonly results = computed(() => this.matches().slice(0, MAX_RESULTS).map((entry) => ({ ...entry, subtitle: entrySubtitle(entry) }))); constructor( private readonly dataLoader: DataLoaderService, @@ -154,7 +153,7 @@ export class SearchComponent { ]); const entries: SearchEntry[] = [ - ...stars.map((star): SearchEntry => ({ kind: 'star', name: star.name, subtitle: spectralClassification(star), starId: star.id })), + ...stars.map((star): SearchEntry => ({ kind: 'star', name: star.name, subtitle: '', star, starId: star.id })), ...bodies.map((body): SearchEntry => ({ kind: 'body', name: body.name, subtitle: body.kind, bodyId: body.id })), ...exoplanets.map((exoplanet): SearchEntry => ({ kind: 'exoplanet', name: exoplanet.name, subtitle: exoplanet.hostStarName, bodyId: exoplanet.id })) ]; diff --git a/src/app/shared/astro/spectral.ts b/src/app/shared/astro/spectral.ts index 1b8362a..03579ea 100644 --- a/src/app/shared/astro/spectral.ts +++ b/src/app/shared/astro/spectral.ts @@ -158,6 +158,13 @@ const DWARF_SEQUENCE: readonly (readonly [string, number, number | null, number, ['M8.5', 2.16, 5.1, 2420, -5.78, -3.09] ]; +/** + * The rows each colour is tabulated for, filtered once. Filtered on every call, the search index + * and the star field's tints, which read the table for each of the 455 571 stars, spent 140-230 + * ms of the main thread on it at boot, and the search index again on each opening of its tab. + */ +const ROWS_WITH_COLOUR = { 1: DWARF_SEQUENCE.filter((row) => row[1] !== null), 2: DWARF_SEQUENCE.filter((row) => row[2] !== null) } as const; + /** * The spectral type of the dwarf whose colour is nearest, for the stars no catalogue classified — * every Gaia star, 83 % of the map. An estimate, and the caller must say so: it assumes a dwarf, @@ -167,7 +174,7 @@ const DWARF_SEQUENCE: readonly (readonly [string, number, number | null, number, */ export function spectralTypeFromColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V'): string | null { const column = system === 'B-V' ? 1 : 2; - const rows = DWARF_SEQUENCE.filter((row) => row[column] !== null); + const rows = ROWS_WITH_COLOUR[column]; if (colorIndex === null || !(colorIndex >= rows[0][column]! && colorIndex <= rows[rows.length - 1][column]!)) { return null; } @@ -213,7 +220,7 @@ export interface DwarfSequencePoint { */ export function dwarfSequenceAtColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V', clampToTable = false): DwarfSequencePoint | null { const column = system === 'B-V' ? 1 : 2; - const rows = DWARF_SEQUENCE.filter((row) => row[column] !== null); + const rows = ROWS_WITH_COLOUR[column]; const [bluest, reddest] = [rows[0][column]!, rows[rows.length - 1][column]!]; if (colorIndex === null || !Number.isFinite(colorIndex) || (!clampToTable && !(colorIndex >= bluest && colorIndex <= reddest))) { return null;