Add frontend performance metrics pipeline to Prometheus
Collect browser-side worker round-trips, computation times, main-thread operations, and feature counts, batch them client-side, and expose as Prometheus histograms via a new POST /api/perf endpoint.
This commit is contained in:
parent
c24c3a545c
commit
d90fa38776
10 changed files with 188 additions and 5 deletions
|
|
@ -9,6 +9,7 @@ from api.auth import get_current_user
|
||||||
from api.config import DEV_TIER_ORIGINS, PROD_TIER_ORIGINS, APP_ENV
|
from api.config import DEV_TIER_ORIGINS, PROD_TIER_ORIGINS, APP_ENV
|
||||||
from api.decision_routes import decision_router
|
from api.decision_routes import decision_router
|
||||||
from api.passkey_routes import passkey_router
|
from api.passkey_routes import passkey_router
|
||||||
|
from api.perf_routes import perf_router
|
||||||
from api.poi_routes import poi_router
|
from api.poi_routes import poi_router
|
||||||
from api.ws_routes import ws_router
|
from api.ws_routes import ws_router
|
||||||
from api.rate_limit_config import RateLimitConfig
|
from api.rate_limit_config import RateLimitConfig
|
||||||
|
|
@ -105,6 +106,7 @@ app = FastAPI(
|
||||||
openapi_url=None if APP_ENV == "production" else "/openapi.json",
|
openapi_url=None if APP_ENV == "production" else "/openapi.json",
|
||||||
)
|
)
|
||||||
app.include_router(passkey_router)
|
app.include_router(passkey_router)
|
||||||
|
app.include_router(perf_router)
|
||||||
app.include_router(poi_router)
|
app.include_router(poi_router)
|
||||||
app.include_router(decision_router)
|
app.include_router(decision_router)
|
||||||
app.include_router(ws_router)
|
app.include_router(ws_router)
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,14 @@ celery_tasks_total: Counter
|
||||||
celery_task_duration_seconds: Histogram
|
celery_task_duration_seconds: Histogram
|
||||||
celery_tasks_active: UpDownCounter
|
celery_tasks_active: UpDownCounter
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Frontend performance metrics
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
frontend_worker_roundtrip: Histogram
|
||||||
|
frontend_worker_compute: Histogram
|
||||||
|
frontend_main_thread: Histogram
|
||||||
|
frontend_feature_count: Histogram
|
||||||
|
|
||||||
|
|
||||||
def init_metrics(service_name: str = "realestate-crawler") -> PrometheusMetricReader:
|
def init_metrics(service_name: str = "realestate-crawler") -> PrometheusMetricReader:
|
||||||
"""Initialise the OTel MeterProvider and define all instruments.
|
"""Initialise the OTel MeterProvider and define all instruments.
|
||||||
|
|
@ -70,6 +78,8 @@ def init_metrics(service_name: str = "realestate-crawler") -> PrometheusMetricRe
|
||||||
global geojson_cache_operations
|
global geojson_cache_operations
|
||||||
global ocr_attempts, ocr_successes
|
global ocr_attempts, ocr_successes
|
||||||
global celery_tasks_total, celery_task_duration_seconds, celery_tasks_active
|
global celery_tasks_total, celery_task_duration_seconds, celery_tasks_active
|
||||||
|
global frontend_worker_roundtrip, frontend_worker_compute
|
||||||
|
global frontend_main_thread, frontend_feature_count
|
||||||
|
|
||||||
if _reader is not None:
|
if _reader is not None:
|
||||||
return _reader
|
return _reader
|
||||||
|
|
@ -144,6 +154,24 @@ def init_metrics(service_name: str = "realestate-crawler") -> PrometheusMetricRe
|
||||||
description="Currently active Celery tasks",
|
description="Currently active Celery tasks",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -- Frontend performance --
|
||||||
|
frontend_worker_roundtrip = _meter.create_histogram(
|
||||||
|
"frontend_worker_roundtrip_seconds",
|
||||||
|
description="Browser worker message round-trip time",
|
||||||
|
)
|
||||||
|
frontend_worker_compute = _meter.create_histogram(
|
||||||
|
"frontend_worker_compute_seconds",
|
||||||
|
description="Computation time inside the web worker",
|
||||||
|
)
|
||||||
|
frontend_main_thread = _meter.create_histogram(
|
||||||
|
"frontend_main_thread_seconds",
|
||||||
|
description="Main-thread blocking operation duration",
|
||||||
|
)
|
||||||
|
frontend_feature_count = _meter.create_histogram(
|
||||||
|
"frontend_feature_count",
|
||||||
|
description="Number of features per heatmap load",
|
||||||
|
)
|
||||||
|
|
||||||
return _reader
|
return _reader
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
43
api/perf_routes.py
Normal file
43
api/perf_routes.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""Frontend performance metrics ingestion endpoint."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
import api.metrics as app_metrics
|
||||||
|
|
||||||
|
ALLOWED_METRICS = {"worker_roundtrip", "worker_compute", "main_thread", "feature_count"}
|
||||||
|
MAX_BATCH_SIZE = 100
|
||||||
|
|
||||||
|
|
||||||
|
class PerfSample(BaseModel):
|
||||||
|
metric: str
|
||||||
|
operation: str = Field(max_length=50)
|
||||||
|
value: float = Field(ge=0, le=3600)
|
||||||
|
|
||||||
|
@field_validator("metric")
|
||||||
|
@classmethod
|
||||||
|
def validate_metric(cls, v: str) -> str:
|
||||||
|
if v not in ALLOWED_METRICS:
|
||||||
|
raise ValueError(f"Unknown metric: {v}")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
perf_router = APIRouter(tags=["perf"])
|
||||||
|
|
||||||
|
|
||||||
|
@perf_router.post("/api/perf", status_code=204)
|
||||||
|
async def record_perf(samples: list[PerfSample]) -> None:
|
||||||
|
if len(samples) > MAX_BATCH_SIZE:
|
||||||
|
samples = samples[:MAX_BATCH_SIZE]
|
||||||
|
|
||||||
|
for s in samples:
|
||||||
|
attrs = {"operation": s.operation}
|
||||||
|
if s.metric == "worker_roundtrip":
|
||||||
|
app_metrics.frontend_worker_roundtrip.record(s.value, attrs)
|
||||||
|
elif s.metric == "worker_compute":
|
||||||
|
app_metrics.frontend_worker_compute.record(s.value, attrs)
|
||||||
|
elif s.metric == "main_thread":
|
||||||
|
app_metrics.frontend_main_thread.record(s.value, attrs)
|
||||||
|
elif s.metric == "feature_count":
|
||||||
|
app_metrics.frontend_feature_count.record(s.value)
|
||||||
|
|
@ -5,6 +5,9 @@ import './index.css';
|
||||||
|
|
||||||
import { AuthProvider } from "react-oidc-context";
|
import { AuthProvider } from "react-oidc-context";
|
||||||
import { oidcConfig } from './auth/config.ts';
|
import { oidcConfig } from './auth/config.ts';
|
||||||
|
import { startCollector } from './services/perfCollector.ts';
|
||||||
|
|
||||||
|
startCollector();
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ export { checkBackendHealth, type HealthStatus, type HealthCheckResult } from '.
|
||||||
export { fetchUserPOIs, createPOI, updatePOI, deletePOI, triggerPOICalculation, fetchPOIDistances, fetchBulkPOIDistances } from './poiService';
|
export { fetchUserPOIs, createPOI, updatePOI, deletePOI, triggerPOICalculation, fetchPOIDistances, fetchBulkPOIDistances } from './poiService';
|
||||||
export { fetchDecisions, setDecision, clearDecision } from './decisionService';
|
export { fetchDecisions, setDecision, clearDecision } from './decisionService';
|
||||||
export { fetchListingDetail } from './listingDetailService';
|
export { fetchListingDetail } from './listingDetailService';
|
||||||
|
export { record as recordPerf, startCollector, stopCollector } from './perfCollector';
|
||||||
|
|
|
||||||
46
frontend/src/services/perfCollector.ts
Normal file
46
frontend/src/services/perfCollector.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
interface PerfSample {
|
||||||
|
metric: string;
|
||||||
|
operation: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FLUSH_INTERVAL_MS = 30_000;
|
||||||
|
let batch: PerfSample[] = [];
|
||||||
|
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
export function record(metric: string, operation: string, value: number): void {
|
||||||
|
batch.push({ metric, operation, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function flush(): void {
|
||||||
|
if (batch.length === 0) return;
|
||||||
|
const blob = new Blob([JSON.stringify(batch)], { type: 'application/json' });
|
||||||
|
batch = [];
|
||||||
|
|
||||||
|
if (navigator.sendBeacon) {
|
||||||
|
navigator.sendBeacon('/api/perf', blob);
|
||||||
|
} else {
|
||||||
|
fetch('/api/perf', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: blob,
|
||||||
|
keepalive: true,
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startCollector(): void {
|
||||||
|
if (flushTimer) return;
|
||||||
|
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'hidden') flush();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopCollector(): void {
|
||||||
|
if (flushTimer) {
|
||||||
|
clearInterval(flushTimer);
|
||||||
|
flushTimer = null;
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import type {
|
||||||
GridResultMessage,
|
GridResultMessage,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { toFeatureItem } from './types';
|
import { toFeatureItem } from './types';
|
||||||
|
import { record } from '../services/perfCollector';
|
||||||
|
|
||||||
interface HexgridConfig {
|
interface HexgridConfig {
|
||||||
intensity: number;
|
intensity: number;
|
||||||
|
|
@ -46,6 +47,9 @@ export class HexgridHeatmapClient {
|
||||||
private pendingColorScale = new Map<number, PendingPromise<ColorScaleResultMessage>>();
|
private pendingColorScale = new Map<number, PendingPromise<ColorScaleResultMessage>>();
|
||||||
private pendingBounds = new Map<number, PendingPromise<BoundsResultMessage>>();
|
private pendingBounds = new Map<number, PendingPromise<BoundsResultMessage>>();
|
||||||
|
|
||||||
|
// Send timestamps for round-trip measurement
|
||||||
|
private sendTimestamps = new Map<number, number>();
|
||||||
|
|
||||||
private config: HexgridConfig = {
|
private config: HexgridConfig = {
|
||||||
intensity: 8,
|
intensity: 8,
|
||||||
spread: 0.1,
|
spread: 0.1,
|
||||||
|
|
@ -118,9 +122,15 @@ export class HexgridHeatmapClient {
|
||||||
|
|
||||||
private handleWorkerMessage(msg: WorkerResponse): void {
|
private handleWorkerMessage(msg: WorkerResponse): void {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'DATA_READY':
|
case 'DATA_READY': {
|
||||||
// Data loaded in worker, nothing to do on main thread
|
const sent = this.sendTimestamps.get(msg.requestId);
|
||||||
|
if (sent !== undefined) {
|
||||||
|
this.sendTimestamps.delete(msg.requestId);
|
||||||
|
record('worker_roundtrip', 'set_data', (performance.now() - sent) / 1000);
|
||||||
|
}
|
||||||
|
record('worker_compute', 'set_data', msg.computeMs / 1000);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'GRID_RESULT':
|
case 'GRID_RESULT':
|
||||||
this.handleGridResult(msg);
|
this.handleGridResult(msg);
|
||||||
|
|
@ -151,6 +161,14 @@ export class HexgridHeatmapClient {
|
||||||
if (msg.requestId !== this.pendingGridRequestId) return;
|
if (msg.requestId !== this.pendingGridRequestId) return;
|
||||||
this.pendingGridRequestId = null;
|
this.pendingGridRequestId = null;
|
||||||
|
|
||||||
|
// Record perf metrics
|
||||||
|
const sent = this.sendTimestamps.get(msg.requestId);
|
||||||
|
if (sent !== undefined) {
|
||||||
|
this.sendTimestamps.delete(msg.requestId);
|
||||||
|
record('worker_roundtrip', 'generate_grid', (performance.now() - sent) / 1000);
|
||||||
|
}
|
||||||
|
record('worker_compute', 'generate_grid', msg.computeMs / 1000);
|
||||||
|
|
||||||
const hexgrid = msg.hexgrid as unknown as GeoJSON.FeatureCollection;
|
const hexgrid = msg.hexgrid as unknown as GeoJSON.FeatureCollection;
|
||||||
const currentZoom = Math.floor(this.map.getZoom());
|
const currentZoom = Math.floor(this.map.getZoom());
|
||||||
const zoomChanged = this.lastZoom !== null && this.lastZoom !== currentZoom;
|
const zoomChanged = this.lastZoom !== null && this.lastZoom !== currentZoom;
|
||||||
|
|
@ -198,12 +216,16 @@ export class HexgridHeatmapClient {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
setData(data: { features: Array<{ properties: any; geometry: { type: 'Point'; coordinates: [number, number] } }> }): void {
|
setData(data: { features: Array<{ properties: any; geometry: { type: 'Point'; coordinates: [number, number] } }> }): void {
|
||||||
// Build main-thread R-tree for click queries
|
// Build main-thread R-tree for click queries
|
||||||
|
const t0 = performance.now();
|
||||||
const items = data.features.map(toFeatureItem);
|
const items = data.features.map(toFeatureItem);
|
||||||
this.mainTree.clear();
|
this.mainTree.clear();
|
||||||
this.mainTree.load(items);
|
this.mainTree.load(items);
|
||||||
|
record('main_thread', 'rtree_build', (performance.now() - t0) / 1000);
|
||||||
|
record('feature_count', 'set_data', data.features.length);
|
||||||
|
|
||||||
// Post to worker for grid generation tree
|
// Post to worker for grid generation tree
|
||||||
const requestId = this.nextRequestId();
|
const requestId = this.nextRequestId();
|
||||||
|
this.sendTimestamps.set(requestId, performance.now());
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
type: 'SET_DATA',
|
type: 'SET_DATA',
|
||||||
requestId,
|
requestId,
|
||||||
|
|
@ -246,8 +268,20 @@ export class HexgridHeatmapClient {
|
||||||
percentileConfig: { minBound: number; maxBound: number }
|
percentileConfig: { minBound: number; maxBound: number }
|
||||||
): Promise<ColorScaleResultMessage> {
|
): Promise<ColorScaleResultMessage> {
|
||||||
const requestId = this.nextRequestId();
|
const requestId = this.nextRequestId();
|
||||||
|
this.sendTimestamps.set(requestId, performance.now());
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.pendingColorScale.set(requestId, { resolve, reject });
|
this.pendingColorScale.set(requestId, {
|
||||||
|
resolve: (msg) => {
|
||||||
|
const sent = this.sendTimestamps.get(requestId);
|
||||||
|
if (sent !== undefined) {
|
||||||
|
this.sendTimestamps.delete(requestId);
|
||||||
|
record('worker_roundtrip', 'color_scale', (performance.now() - sent) / 1000);
|
||||||
|
}
|
||||||
|
record('worker_compute', 'color_scale', msg.computeMs / 1000);
|
||||||
|
resolve(msg);
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
});
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
type: 'COMPUTE_COLOR_SCALE',
|
type: 'COMPUTE_COLOR_SCALE',
|
||||||
requestId,
|
requestId,
|
||||||
|
|
@ -261,8 +295,20 @@ export class HexgridHeatmapClient {
|
||||||
boundsPercentileConfig: { clipMin: number; clipMax: number }
|
boundsPercentileConfig: { clipMin: number; clipMax: number }
|
||||||
): Promise<BoundsResultMessage> {
|
): Promise<BoundsResultMessage> {
|
||||||
const requestId = this.nextRequestId();
|
const requestId = this.nextRequestId();
|
||||||
|
this.sendTimestamps.set(requestId, performance.now());
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.pendingBounds.set(requestId, { resolve, reject });
|
this.pendingBounds.set(requestId, {
|
||||||
|
resolve: (msg) => {
|
||||||
|
const sent = this.sendTimestamps.get(requestId);
|
||||||
|
if (sent !== undefined) {
|
||||||
|
this.sendTimestamps.delete(requestId);
|
||||||
|
record('worker_roundtrip', 'bounds', (performance.now() - sent) / 1000);
|
||||||
|
}
|
||||||
|
record('worker_compute', 'bounds', msg.computeMs / 1000);
|
||||||
|
resolve(msg);
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
});
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
type: 'COMPUTE_BOUNDS',
|
type: 'COMPUTE_BOUNDS',
|
||||||
requestId,
|
requestId,
|
||||||
|
|
@ -302,6 +348,7 @@ export class HexgridHeatmapClient {
|
||||||
|
|
||||||
const requestId = this.nextRequestId();
|
const requestId = this.nextRequestId();
|
||||||
this.pendingGridRequestId = requestId;
|
this.pendingGridRequestId = requestId;
|
||||||
|
this.sendTimestamps.set(requestId, performance.now());
|
||||||
|
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
type: 'GENERATE_GRID',
|
type: 'GENERATE_GRID',
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ function reduceAverage(data: number[]): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateGrid(msg: GenerateGridMessage): void {
|
function generateGrid(msg: GenerateGridMessage): void {
|
||||||
|
const t0 = performance.now();
|
||||||
const { requestId, zoom, bounds, config } = msg;
|
const { requestId, zoom, bounds, config } = msg;
|
||||||
const { cellDensity, spread, propertyName } = config;
|
const { cellDensity, spread, propertyName } = config;
|
||||||
|
|
||||||
|
|
@ -115,11 +116,13 @@ function generateGrid(msg: GenerateGridMessage): void {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'GRID_RESULT',
|
type: 'GRID_RESULT',
|
||||||
requestId,
|
requestId,
|
||||||
|
computeMs: performance.now() - t0,
|
||||||
hexgrid: { type: 'FeatureCollection', features: cellsToSave },
|
hexgrid: { type: 'FeatureCollection', features: cellsToSave },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeColorScale(msg: ComputeColorScaleMessage): void {
|
function computeColorScale(msg: ComputeColorScaleMessage): void {
|
||||||
|
const t0 = performance.now();
|
||||||
const { requestId, metricMode, percentileConfig } = msg;
|
const { requestId, metricMode, percentileConfig } = msg;
|
||||||
|
|
||||||
const values: number[] = [];
|
const values: number[] = [];
|
||||||
|
|
@ -143,6 +146,7 @@ function computeColorScale(msg: ComputeColorScaleMessage): void {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'COLOR_SCALE_RESULT',
|
type: 'COLOR_SCALE_RESULT',
|
||||||
requestId,
|
requestId,
|
||||||
|
computeMs: performance.now() - t0,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
hasValues: true,
|
hasValues: true,
|
||||||
|
|
@ -151,6 +155,7 @@ function computeColorScale(msg: ComputeColorScaleMessage): void {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'COLOR_SCALE_RESULT',
|
type: 'COLOR_SCALE_RESULT',
|
||||||
requestId,
|
requestId,
|
||||||
|
computeMs: performance.now() - t0,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
hasValues: false,
|
hasValues: false,
|
||||||
|
|
@ -159,6 +164,7 @@ function computeColorScale(msg: ComputeColorScaleMessage): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeBounds(msg: ComputeBoundsMessage): void {
|
function computeBounds(msg: ComputeBoundsMessage): void {
|
||||||
|
const t0 = performance.now();
|
||||||
const { requestId, boundsPercentileConfig } = msg;
|
const { requestId, boundsPercentileConfig } = msg;
|
||||||
|
|
||||||
const lngs: number[] = [];
|
const lngs: number[] = [];
|
||||||
|
|
@ -178,6 +184,7 @@ function computeBounds(msg: ComputeBoundsMessage): void {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'BOUNDS_RESULT',
|
type: 'BOUNDS_RESULT',
|
||||||
requestId,
|
requestId,
|
||||||
|
computeMs: performance.now() - t0,
|
||||||
minLng: percentile(lngs, clipMin),
|
minLng: percentile(lngs, clipMin),
|
||||||
maxLng: percentile(lngs, clipMax),
|
maxLng: percentile(lngs, clipMax),
|
||||||
minLat: percentile(lats, clipMin),
|
minLat: percentile(lats, clipMin),
|
||||||
|
|
@ -205,6 +212,7 @@ self.onmessage = (e: MessageEvent<WorkerRequest>) => {
|
||||||
|
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'SET_DATA': {
|
case 'SET_DATA': {
|
||||||
|
const t0 = performance.now();
|
||||||
features = msg.features.map(toFeatureItem);
|
features = msg.features.map(toFeatureItem);
|
||||||
tree.clear();
|
tree.clear();
|
||||||
tree.load(features);
|
tree.load(features);
|
||||||
|
|
@ -213,6 +221,7 @@ self.onmessage = (e: MessageEvent<WorkerRequest>) => {
|
||||||
type: 'DATA_READY',
|
type: 'DATA_READY',
|
||||||
requestId: msg.requestId,
|
requestId: msg.requestId,
|
||||||
featureCount: features.length,
|
featureCount: features.length,
|
||||||
|
computeMs: performance.now() - t0,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,11 +70,13 @@ export interface DataReadyMessage {
|
||||||
type: 'DATA_READY';
|
type: 'DATA_READY';
|
||||||
requestId: number;
|
requestId: number;
|
||||||
featureCount: number;
|
featureCount: number;
|
||||||
|
computeMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridResultMessage {
|
export interface GridResultMessage {
|
||||||
type: 'GRID_RESULT';
|
type: 'GRID_RESULT';
|
||||||
requestId: number;
|
requestId: number;
|
||||||
|
computeMs: number;
|
||||||
hexgrid: {
|
hexgrid: {
|
||||||
type: 'FeatureCollection';
|
type: 'FeatureCollection';
|
||||||
features: Array<{
|
features: Array<{
|
||||||
|
|
@ -94,6 +96,7 @@ export interface GridResultMessage {
|
||||||
export interface ColorScaleResultMessage {
|
export interface ColorScaleResultMessage {
|
||||||
type: 'COLOR_SCALE_RESULT';
|
type: 'COLOR_SCALE_RESULT';
|
||||||
requestId: number;
|
requestId: number;
|
||||||
|
computeMs: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
hasValues: boolean;
|
hasValues: boolean;
|
||||||
|
|
@ -102,6 +105,7 @@ export interface ColorScaleResultMessage {
|
||||||
export interface BoundsResultMessage {
|
export interface BoundsResultMessage {
|
||||||
type: 'BOUNDS_RESULT';
|
type: 'BOUNDS_RESULT';
|
||||||
requestId: number;
|
requestId: number;
|
||||||
|
computeMs: number;
|
||||||
minLng: number;
|
minLng: number;
|
||||||
maxLng: number;
|
maxLng: number;
|
||||||
minLat: number;
|
minLat: number;
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"root":["./src/app.tsx","./src/appsidebar.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/auth/authservice.ts","./src/auth/config.ts","./src/auth/errors.ts","./src/auth/passkeyservice.ts","./src/auth/types.ts","./src/components/activequery.tsx","./src/components/alerterror.tsx","./src/components/authcallback.tsx","./src/components/favoritesview.tsx","./src/components/filterpanel.tsx","./src/components/header.tsx","./src/components/healthindicator.tsx","./src/components/listview.tsx","./src/components/listingdetail.tsx","./src/components/listingdetailsheet.tsx","./src/components/loginmodal.tsx","./src/components/map.tsx","./src/components/mobilebottomsheet.tsx","./src/components/mobilemenu.tsx","./src/components/poimanager.tsx","./src/components/photocarousel.tsx","./src/components/propertycard.tsx","./src/components/propertycardcompact.tsx","./src/components/savedview.tsx","./src/components/spinner.tsx","./src/components/statsbar.tsx","./src/components/streamingprogressbar.tsx","./src/components/swipecard.tsx","./src/components/swipereviewmode.tsx","./src/components/swipeablecardrow.tsx","./src/components/swipeablepropertycard.tsx","./src/components/taskindicator.tsx","./src/components/taskprogressdrawer.tsx","./src/components/visualizationcard.tsx","./src/components/ui/datepicker.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/dialog.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/range-slider-field.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/constants/colorschemes.ts","./src/constants/index.ts","./src/hooks/use-mobile.ts","./src/hooks/usedecisions.ts","./src/hooks/uselistingdetail.ts","./src/hooks/usetaskprogress.ts","./src/lib/utils.ts","./src/services/apiclient.ts","./src/services/decisionservice.ts","./src/services/healthservice.ts","./src/services/index.ts","./src/services/listingdetailservice.ts","./src/services/listingservice.ts","./src/services/poiservice.ts","./src/services/streamingservice.ts","./src/services/taskservice.ts","./src/types/index.ts","./src/utils/maputils.ts","./src/utils/poiutils.ts","./src/workers/hexgridheatmapclient.ts","./src/workers/hexgrid.worker.ts","./src/workers/types.ts"],"version":"5.8.3"}
|
{"root":["./src/app.tsx","./src/appsidebar.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/auth/authservice.ts","./src/auth/config.ts","./src/auth/errors.ts","./src/auth/passkeyservice.ts","./src/auth/types.ts","./src/components/activequery.tsx","./src/components/alerterror.tsx","./src/components/authcallback.tsx","./src/components/favoritesview.tsx","./src/components/filterpanel.tsx","./src/components/header.tsx","./src/components/healthindicator.tsx","./src/components/listview.tsx","./src/components/listingdetail.tsx","./src/components/listingdetailsheet.tsx","./src/components/loginmodal.tsx","./src/components/map.tsx","./src/components/mobilebottomsheet.tsx","./src/components/mobilemenu.tsx","./src/components/poimanager.tsx","./src/components/photocarousel.tsx","./src/components/propertycard.tsx","./src/components/propertycardcompact.tsx","./src/components/savedview.tsx","./src/components/spinner.tsx","./src/components/statsbar.tsx","./src/components/streamingprogressbar.tsx","./src/components/swipecard.tsx","./src/components/swipereviewmode.tsx","./src/components/swipeablecardrow.tsx","./src/components/swipeablepropertycard.tsx","./src/components/taskindicator.tsx","./src/components/taskprogressdrawer.tsx","./src/components/visualizationcard.tsx","./src/components/ui/datepicker.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/dialog.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/range-slider-field.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/constants/colorschemes.ts","./src/constants/index.ts","./src/hooks/use-mobile.ts","./src/hooks/usedecisions.ts","./src/hooks/uselistingdetail.ts","./src/hooks/usetaskprogress.ts","./src/lib/utils.ts","./src/services/apiclient.ts","./src/services/decisionservice.ts","./src/services/healthservice.ts","./src/services/index.ts","./src/services/listingdetailservice.ts","./src/services/listingservice.ts","./src/services/perfcollector.ts","./src/services/poiservice.ts","./src/services/streamingservice.ts","./src/services/taskservice.ts","./src/types/index.ts","./src/utils/maputils.ts","./src/utils/poiutils.ts","./src/workers/hexgridheatmapclient.ts","./src/workers/hexgrid.worker.ts","./src/workers/types.ts"],"version":"5.8.3"}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue