mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
perf(onglets): liste des familles calculée dans un worker
Quand l'arbre affiché est celui de la famille de départ, il ne dépend pas de la liste des onglets : il s'affiche tout de suite, la liste est calculée dans galaxy/familyTabsWorker.js et la barre suit, sans geler la page. Les pastilles prennent le nom et le compte de leur onglet à l'arrivée de la liste. Calcul sur place comme avant dans un autre onglet, à l'arrivée par le lien d'un onglet, sans Worker ou si le worker échoue (couples de l'arbre affiché remis). Une demande en cours est abandonnée dès qu'une autre part. Royal92 (3 010 personnes) : arbre utilisable en 1,6 s au lieu de 8,7 s, barre des onglets à 6 s sans bloquer (médianes de 3, machine chargée). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14f62aee90
commit
d7eac40cff
3 files changed
+103
-9
No files matched your search
@@ -2,7 +2,7 @@ import { memo, useEffect, useImperativeHandle, useRef, useState, useCallback } f
|
||||
import { getPersonMedias, getParents, getChildren, getPersonUnions, persons as graphPersons, unions as graphUnions, filiations as graphFiliations } from '../data/mockData'
|
||||
import { PERSON_R, FLOAT_SPEED, FLOAT_AMPLITUDE, REPULSION_RADIUS, REPULSION_STRENGTH, MAX_ORBIT_MEDIAS, ENTRANCE_GEN_DELAY, ENTRANCE_NODE_DURATION, ENTRANCE_NODE_STAGGER, ENTRANCE_MAX_GEN_SPAN, ENTRANCE_MAX_NODE_SPAN, ENTRANCE_LINE_DELAY, ENTRANCE_LINE_DURATION, ENTRANCE_ORBIT_DELAY, ENTRANCE_ORBIT_DURATION, ENTRANCE_ORBIT_STAGGER } from './galaxy/constants'
|
||||
import { easeOutCubic } from './galaxy/utils'
|
||||
import { computeLayout, generateNodeRandomData, coupleBarMeta } from './galaxy/elkLayout'
|
||||
import { computeLayout, generateNodeRandomData, coupleBarMeta, restoreCoupleBarMeta } from './galaxy/elkLayout'
|
||||
import { computeBlockLayout, familyTabsOf, lineToFamily, subgraphOf, takeRootLayout } from './galaxy/blockLayout'
|
||||
import { hasGhosts, withGhosts } from '../utils/ghosts'
|
||||
|
||||
@@ -53,6 +53,15 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
const [tabs, setTabs] = useState([])
|
||||
const [activeKey, setActiveKey] = useState(focusPersonId != null ? String(focusPersonId) : null)
|
||||
const tabsCacheRef = useRef({ rev: -1, start: undefined, tabs: [] })
|
||||
// Liste des onglets calculée dans un worker (galaxy/familyTabsWorker.js) quand l'arbre affiché est celui de la
|
||||
// famille de départ : il ne dépend pas de cette liste, il s'affiche tout de suite et la barre suit. Une demande
|
||||
// en cours est abandonnée (worker arrêté) dès qu'une autre part ou qu'on calcule la liste sur place.
|
||||
const tabsRequestRef = useRef(null) // { rev, start, worker }
|
||||
const cancelTabsRequest = useCallback(() => {
|
||||
tabsRequestRef.current?.worker.terminate()
|
||||
tabsRequestRef.current = null
|
||||
}, [])
|
||||
useEffect(() => cancelTabsRequest, [cancelTabsRequest])
|
||||
const pendingFocusRef = useRef(null) // personne cherchée dans un autre onglet, visée après le changement
|
||||
// Lien d'un onglet (?famille=<charnière>) : ouvert dès que l'arbre et ses onglets sont connus, et de nouveau si
|
||||
// la personne de départ arrive après (visiteur), tant qu'on n'a pas soi-même changé d'onglet
|
||||
@@ -493,6 +502,51 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Liste arrivée : l'arbre affiché (famille de départ) ne change pas, ses pastilles prennent le nom et le compte de
|
||||
// l'onglet où elles mènent
|
||||
const applyTabs = useCallback((req, list) => {
|
||||
if (tabsRequestRef.current !== req) return
|
||||
tabsRequestRef.current = null
|
||||
tabsCacheRef.current = { rev: req.rev, start: req.start, tabs: list }
|
||||
setTabs(list)
|
||||
for (const n of layoutDataRef.current?.children || []) {
|
||||
const target = n._familyTab && list.find((tb) => tb.key === n._familyTab.tabKey)
|
||||
if (target) n._familyTab = { ...n._familyTab, label: target.label, count: target.count }
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
}, [])
|
||||
|
||||
// Lance le calcul de la liste dans un worker ; faux si le navigateur ne le permet pas (on calcule alors sur place)
|
||||
const requestTabs = useCallback((rev, start, referenceId) => {
|
||||
cancelTabsRequest()
|
||||
if (typeof Worker === 'undefined') return false
|
||||
let worker
|
||||
try {
|
||||
worker = new Worker(new URL('./galaxy/familyTabsWorker.js', import.meta.url), { type: 'module' })
|
||||
worker.postMessage({ graph: { persons: graphPersons, unions: graphUnions, filiations: graphFiliations }, referenceId })
|
||||
} catch {
|
||||
worker?.terminate()
|
||||
return false
|
||||
}
|
||||
const req = { rev, start, worker }
|
||||
tabsRequestRef.current = req
|
||||
// Échec du worker : la liste est calculée sur place, sans toucher aux couples de l'arbre affiché
|
||||
const fallback = () => {
|
||||
if (tabsRequestRef.current !== req) return
|
||||
const meta = coupleBarMeta
|
||||
const list = familyTabsOf(null, referenceId)
|
||||
restoreCoupleBarMeta(meta)
|
||||
applyTabs(req, list)
|
||||
}
|
||||
worker.onmessage = ({ data }) => {
|
||||
worker.terminate()
|
||||
if (data.error) fallback()
|
||||
else applyTabs(req, data.tabs)
|
||||
}
|
||||
worker.onerror = () => { worker.terminate(); fallback() }
|
||||
return true
|
||||
}, [cancelTabsRequest, applyTabs])
|
||||
|
||||
// Layout
|
||||
useEffect(() => {
|
||||
// Onglets : la liste des familles, une fois par version du graphe et par personne de départ. Calculée
|
||||
@@ -500,12 +554,27 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
const start = startRefRef.current != null ? String(startRefRef.current) : null
|
||||
const cache = tabsCacheRef.current
|
||||
let freshTabs = null
|
||||
// Liste en attente du worker : l'arbre affiché est celui de la famille de départ
|
||||
let tabsPending = false
|
||||
if (USE_BLOCK_LAYOUT && familyTabs && (cache.rev !== graphRevision || cache.start !== start)) {
|
||||
freshTabs = familyTabsOf(null, startRefRef.current)
|
||||
tabsCacheRef.current = { rev: graphRevision, start, tabs: freshTabs }
|
||||
setTabs(freshTabs)
|
||||
const pending = tabsRequestRef.current
|
||||
// Dans un onglet d'une autre famille, ou à l'arrivée par le lien d'un onglet, l'arbre affiché dépend de la
|
||||
// liste : elle est calculée sur place
|
||||
const inOtherTab = Boolean(cache.tabs.find((tb) => tb.key === activeKey)?.personIds)
|
||||
if (!inOtherTab && pendingFamilyRef.current == null) {
|
||||
tabsPending = (pending && pending.rev === graphRevision && pending.start === start) || requestTabs(graphRevision, start, startRefRef.current)
|
||||
}
|
||||
if (!tabsPending) {
|
||||
cancelTabsRequest()
|
||||
freshTabs = familyTabsOf(null, startRefRef.current)
|
||||
tabsCacheRef.current = { rev: graphRevision, start, tabs: freshTabs }
|
||||
setTabs(freshTabs)
|
||||
}
|
||||
}
|
||||
if (!familyTabs) {
|
||||
cancelTabsRequest()
|
||||
if (tabsCacheRef.current.tabs.length) { tabsCacheRef.current = { rev: -1, start: undefined, tabs: [] }; setTabs([]) }
|
||||
}
|
||||
if (!familyTabs && tabsCacheRef.current.tabs.length) { tabsCacheRef.current = { rev: -1, start: undefined, tabs: [] }; setTabs([]) }
|
||||
// La famille du lien : son onglet s'il existe dans ce navigateur, sinon l'arbre entier ouvert sur sa charnière
|
||||
if (pendingFamilyRef.current != null && graphPersons.length) {
|
||||
const key = pendingFamilyRef.current
|
||||
@@ -515,7 +584,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
if (key !== activeKey) { setActiveKey(key); return }
|
||||
} else frameOnRef.current = key
|
||||
}
|
||||
const tab = familyTabs ? (tabsCacheRef.current.tabs.find((tb) => tb.key === activeKey) || tabsCacheRef.current.tabs[0] || null) : null
|
||||
const tab = familyTabs && !tabsPending ? (tabsCacheRef.current.tabs.find((tb) => tb.key === activeKey) || tabsCacheRef.current.tabs[0] || null) : null
|
||||
activeTabRef.current = tab
|
||||
// La personne de départ (ou la charnière de l'onglet) sert de référence aux rangées
|
||||
focusPersonIdRef.current = tab ? tab.referenceId : startRefRef.current
|
||||
@@ -688,7 +757,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
pendingFocusRef.current = null
|
||||
requestAnimationFrame(() => zoomApiRef?.current?.focusPerson?.(id))
|
||||
}
|
||||
}, [graphRevision, activeKey, familyTabs, selfPersonId, ghosts])
|
||||
}, [graphRevision, activeKey, familyTabs, selfPersonId, ghosts, requestTabs, cancelTabsRequest])
|
||||
|
||||
// Onglet ouvert, remonté pour l'adresse de la page et le lien de partage ; null pour la famille de départ
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { computeBlockLayout, familyTabsOf, filCroiseAutreFamille, lineToFamily, takeRootLayout } from './blockLayout'
|
||||
import * as elk from './elkLayout'
|
||||
import { loadGraphData } from '../../data/mockData'
|
||||
import * as mockData from '../../data/mockData'
|
||||
import { generate } from '../../../../tools/layout-bench/gen-fr.mjs'
|
||||
import { COMPACT_W, PERSON_W } from './constants'
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('lineToFamily, ligne directe vers un onglet de famille', () => {
|
||||
describe('familyTabsOf, placement de la famille de départ gardé', () => {
|
||||
it('rend le même placement et les mêmes couples qu’un calcul neuf, une seule fois', () => {
|
||||
const tree = generate('desc', 5)
|
||||
loadGraphData(tree.graph, { apiBaseUrl: '' })
|
||||
mockData.loadGraphData(tree.graph, { apiBaseUrl: '' })
|
||||
const fresh = computeBlockLayout(null, tree.rootPersonId)
|
||||
const freshMeta = JSON.stringify(elk.coupleBarMeta)
|
||||
const tabs = familyTabsOf(null, tree.rootPersonId)
|
||||
@@ -186,3 +186,16 @@ describe('familyTabsOf, placement de la famille de départ gardé', () => {
|
||||
expect(takeRootLayout(tabs, tree.rootPersonId)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('familyTabsOf, graphe passé en entier (worker)', () => {
|
||||
it('rend la même liste d’onglets sans les données du module', () => {
|
||||
const tree = generate('desc', 5)
|
||||
mockData.loadGraphData(tree.graph, { apiBaseUrl: '' })
|
||||
const graph = structuredClone({ persons: mockData.persons, unions: mockData.unions, filiations: mockData.filiations })
|
||||
const expected = familyTabsOf(null, tree.rootPersonId)
|
||||
mockData.replaceGraph({ persons: [], unions: [], filiations: [] })
|
||||
const fromWorker = familyTabsOf(graph, tree.rootPersonId)
|
||||
expect(fromWorker.length).toBeGreaterThan(1)
|
||||
expect(fromWorker).toEqual(expected)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
// Liste des onglets de familles calculée hors du fil principal : sur un arbre de quelques milliers de personnes,
|
||||
// familyTabsOf place chaque onglet pour trouver ceux qu'il cache, plusieurs secondes pendant lesquelles la page
|
||||
// ne répondrait plus. Le graphe arrive avec la demande (ce module n'a pas les données de l'arbre).
|
||||
import { familyTabsOf } from './blockLayout'
|
||||
|
||||
self.onmessage = ({ data: { id, graph, referenceId } }) => {
|
||||
try {
|
||||
self.postMessage({ id, tabs: familyTabsOf(graph, referenceId) })
|
||||
} catch (error) {
|
||||
self.postMessage({ id, error: String(error?.message || error) })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user