lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain pruefungLaufzeit.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { validateProject } from '@/domain/validation/engine'; |
| 3 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 4 | import { |
| 5 | createArm, |
| 6 | createConflict, |
| 7 | createEmptyProject, |
| 8 | createPhase, |
| 9 | createSignalGroup, |
| 10 | } from '@/domain/model/factory'; |
| 11 | import type { Conflict, Project } from '@/domain/model/project'; |
| 12 | |
| 13 | /* |
| 14 | * Strittiger Befund S2 (Fassung 5.11.0): Die Pruefung tastete je |
| 15 | * Konfliktbeziehung die GANZE Konfliktliste erneut ab. |
| 16 | * |
| 17 | * engine.ts, checkConflicts: `if (!project.conflicts.some((c) => |
| 18 | * c.fromId === conflict.toId && c.toId === conflict.fromId)) {` |
| 19 | * |
| 20 | * Die Zeile steht in der Schleife `for (const conflict of project.conflicts)` |
| 21 | * und sucht die Gegenrichtung jedes Mal von vorn. Der Aufwand waechst damit |
| 22 | * quadratisch in der Zahl der Beziehungen und, weil diese bei erfasster Matrix |
| 23 | * selbst quadratisch mit der Zahl der Signalgruppen waechst, in vierter Potenz |
| 24 | * der Gruppenzahl. `validateProject` laeuft ueber `ProjectStore.applyProject` |
| 25 | * bei JEDER Aenderung synchron mit; weder das Einlesen noch die Oberflaeche |
| 26 | * setzen eine Obergrenze fuer die Zahl der Signalgruppen. |
| 27 | * |
| 28 | * GEMESSEN am Altstand (dieselbe Anlage, nur groesser; Zugriffe auf die |
| 29 | * Konfliktliste je Beziehung): K = 50 -> 29,5 · K = 200 -> 104,5 · |
| 30 | * K = 800 -> 404,5 · K = 3200 -> 1604,5. Der Anteil ist exakt K/2 + 4,5, also |
| 31 | * genau die halbe Liste je Beziehung. Laufzeit von `validateProject` allein: |
| 32 | * 200 Signalgruppen, 20 000 Beziehungen: 2,2 s je Aenderung. |
| 33 | * |
| 34 | * DIE WACHE ZAEHLT STATT ZU MESSEN: Eine Zeitschranke haengt am Rechner, der |
| 35 | * sie ausfuehrt. Gezaehlt werden deshalb die Elementzugriffe auf die |
| 36 | * Konfliktliste - dieselbe Groesse, aber vom Rechner unabhaengig. Die Zahl je |
| 37 | * Beziehung muss beschraenkt bleiben, statt mit der Liste zu wachsen. |
| 38 | * |
| 39 | * KEINE ZAHL AENDERT SICH: Die Pruefung liefert vorher wie nachher dieselben |
| 40 | * Befunde; der Nachweis steht als eigener Fall darunter. |
| 41 | * |
| 42 | * GEGEN DEN ALTSTAND schlagen die beiden ersten Faelle fehl (404,5 bzw. |
| 43 | * 1604,5 Zugriffe je Beziehung); der dritte besteht dort ebenso. |
| 44 | */ |
| 45 | |
| 46 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 47 | |
| 48 | /** |
| 49 | * Knotenpunkt mit `n` Signalgruppen in zwei Phasen und vollstaendig erfasster |
| 50 | * Konfliktmatrix ueber die Phasengrenze - in beiden Richtungen, also der |
| 51 | * fehlerfreie Regelfall, in dem die Gegenrichtungsprobe die Liste bis zum Ende |
| 52 | * durchlaeuft. |
| 53 | */ |
| 54 | function knotenpunkt(n: number): Project { |
| 55 | const base = createEmptyProject('Grosse Anlage', DATUM, 'knotenpunkt'); |
| 56 | const arm = createArm('Nord', 'Nord', 2, 50); |
| 57 | const gruppen = Array.from({ length: n }, (_, i) => |
| 58 | createSignalGroup({ name: `K${i + 1}`, mode: 'kfz', armId: arm.id, index: i }), |
| 59 | ); |
| 60 | const erste = gruppen.filter((_, i) => i % 2 === 0); |
| 61 | const zweite = gruppen.filter((_, i) => i % 2 === 1); |
| 62 | const p1 = createPhase( |
| 63 | 'Phase 1', |
| 64 | erste.map((g) => g.id), |
| 65 | ); |
| 66 | const p2 = createPhase( |
| 67 | 'Phase 2', |
| 68 | zweite.map((g) => g.id), |
| 69 | ); |
| 70 | const conflicts: Conflict[] = []; |
| 71 | for (const a of erste) { |
| 72 | for (const b of zweite) { |
| 73 | conflicts.push({ ...createConflict(a.id, b.id), clearingDistance: 22, enteringDistance: 8 }); |
| 74 | conflicts.push({ ...createConflict(b.id, a.id), clearingDistance: 22, enteringDistance: 8 }); |
| 75 | } |
| 76 | } |
| 77 | return { |
| 78 | ...base, |
| 79 | intersection: { ...base.intersection, arms: [arm] }, |
| 80 | signalGroups: gruppen, |
| 81 | conflicts, |
| 82 | phases: [p1, p2], |
| 83 | program: { ...base.program, phaseOrder: [p1.id, p2.id] }, |
| 84 | }; |
| 85 | } |
| 86 | |
| 87 | /** Elementzugriffe auf `project.conflicts` waehrend einer Pruefung, je Beziehung. */ |
| 88 | function zugriffeJeBeziehung(projekt: Project): number { |
| 89 | const plan = buildSignalPlan(projekt); |
| 90 | let zugriffe = 0; |
| 91 | const beobachtet = new Proxy(projekt.conflicts as Conflict[], { |
| 92 | get(ziel, eigenschaft, empfaenger) { |
| 93 | if (typeof eigenschaft === 'string' && /^\d+$/.test(eigenschaft)) zugriffe += 1; |
| 94 | return Reflect.get(ziel, eigenschaft, empfaenger) as unknown; |
| 95 | }, |
| 96 | }); |
| 97 | validateProject({ ...projekt, conflicts: beobachtet }, plan, DATUM); |
| 98 | return zugriffe / projekt.conflicts.length; |
| 99 | } |
| 100 | |
| 101 | describe('Aufwand der Pruefung bei vielen Signalgruppen', () => { |
| 102 | it('liest die Konfliktliste nicht je Beziehung einmal ganz durch', () => { |
| 103 | const projekt = knotenpunkt(40); |
| 104 | // Voraussetzung des Aufbaus: eine Liste, die gross genug ist, um den |
| 105 | // Unterschied zu zeigen. |
| 106 | expect(projekt.conflicts).toHaveLength(800); |
| 107 | expect(zugriffeJeBeziehung(projekt)).toBeLessThan(20); |
| 108 | }); |
| 109 | |
| 110 | it('haelt den Aufwand je Beziehung, wenn die Anlage waechst', () => { |
| 111 | const klein = zugriffeJeBeziehung(knotenpunkt(40)); |
| 112 | const gross = zugriffeJeBeziehung(knotenpunkt(80)); |
| 113 | // Viermal so viele Beziehungen duerfen den Aufwand JE BEZIEHUNG nicht |
| 114 | // vervierfachen; er darf ueberhaupt nicht mit der Listenlaenge wachsen. |
| 115 | expect(gross).toBeLessThanOrEqual(klein + 1); |
| 116 | }); |
| 117 | |
| 118 | it('meldet dabei unveraendert dieselben Befunde', () => { |
| 119 | const projekt = knotenpunkt(12); |
| 120 | const bericht = validateProject(projekt, buildSignalPlan(projekt), DATUM); |
| 121 | // Erfasste Gegenrichtungen: keine Meldung. Sobald eine fehlt, meldet die |
| 122 | // Regel sie - genau einmal, fuer die verbliebene Richtung. |
| 123 | expect(bericht.findings.some((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toBe(false); |
| 124 | |
| 125 | const halb: Project = { ...projekt, conflicts: projekt.conflicts.slice(0, -1) }; |
| 126 | const fehlend = validateProject(halb, buildSignalPlan(halb), DATUM); |
| 127 | expect(fehlend.findings.filter((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toHaveLength( |
| 128 | 1, |
| 129 | ); |
| 130 | }); |
| 131 | }); |