Switch to Arrow Flight#15
Conversation
…nar parsing
- Backend: arrow_service serializes telemetry datasets to Arrow IPC
streams and exposes a GridifyFlightServer (Arrow Flight) plus
/api/telemetry/arrow/{dataset} HTTP endpoints serving binary
columnar data with application/vnd.apache.arrow.stream.
- Frontend: apache-arrow client unpacks the binary IPC stream directly
into chart datasets (no JSON.parse), with an ArrowTelemetry demo
wired into the analytics view and unit tests.
…eddings - DuckDB-WASM runs inside a web worker (duckdbClient.ts) so filter/ sort/micro-aggregation of cached telemetry is executed locally in the browser, off the centralized FastAPI/Celery cluster. Pure SQL builders (duckdbQueries.ts) whitelist columns/directions to prevent injection. - Hybrid RAG (ragClient.ts) embeds queries with a lazy ONNX model (onnxruntime-web, dynamically imported from a CDN) and cosine-matches them against the cached semantic index in-browser before hitting the cloud Chroma store, with a deterministic hashing fallback for offline/tests. - Demo components + unit tests wired into the analytics view.
…r guardrails - LLM cache now stores Pydantic-validated dashboard schemas directly (set_validated/get_validated); repetitive queries return the verified model without re-running schema validation, cutting CPU on hot paths. - Prompt-injection guardrails moved out of the synchronous Gemini handler into an async ASGI middleware (AsyncGuardrailMiddleware). Heuristic scans run off the event loop via to_thread; when GUARDRAILS_EDGE_URL is set the check delegates to a lightweight edge microservice/API gateway, keeping time-to-first-token responsive. - gemini_service now caches/serves the validated DashboardCommandResult; gemini router drops its inline guardrail block.
- Dashboard canvas migrated to a native CSS Grid (gridify-canvas / gridify-col-N) driven by Tailwind 4 @theme tokens. Widget spans come from each widget's column count; reflow is handled by the browser grid engine, and the Framer Motion layout prop on widgets was removed so resizing charts no longer triggers JS layout recalculation. - ECharts now imports from echarts/core and registers only the exact Line/Bar/Scatter/Heatmap/Treemap charts plus Grid/Tooltip/VisualMap components and the Canvas renderer via echarts-for-react/lib/core, cutting the ECharts chunk from ~1.06 MB to ~594 KB (gzip 350 -> 199 KB). - Added vitest.config.ts so npm test runs only unit tests and excludes the Playwright e2e specs.
|
Warning Review limit reached
Next review available in: 22 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (30)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR switches telemetry and analytics paths toward Arrow-based browser processing. The main changes are:
Confidence Score: 4/5The DuckDB analytics path can return duplicated rows after the analytics view remounts.
src/lib/duckdbClient.ts, backend/app/routers/telemetry.py Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
Telemetry[FastAPI telemetry data] --> Arrow[Arrow IPC endpoints]
Arrow --> ArrowUI[ArrowTelemetry UI]
Arrow --> DuckUI[DuckDBAnalytics UI]
DuckUI --> DuckDB[DuckDB-WASM singleton]
DuckDB --> Queries[Local filters and aggregates]
GeminiReq[Gemini command POST] --> Guardrails[Async guardrail middleware]
Guardrails --> Gemini[Gemini router]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart LR
Telemetry[FastAPI telemetry data] --> Arrow[Arrow IPC endpoints]
Arrow --> ArrowUI[ArrowTelemetry UI]
Arrow --> DuckUI[DuckDBAnalytics UI]
DuckUI --> DuckDB[DuckDB-WASM singleton]
DuckDB --> Queries[Local filters and aggregates]
GeminiReq[Gemini command POST] --> Guardrails[Async guardrail middleware]
Guardrails --> Gemini[Gemini router]
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
src/lib/duckdbClient.ts:65
**DuckDB Rows Append On Remount**
`DuckDBAnalytics` is mounted only while the analytics view is active, but `duckDBAnalytics` is a singleton and `loadDevices()` only inserts into the existing `devices` table. Switching away and back resets the hook's `loadedKey` and inserts the same Arrow rows again, so filters and aggregates return duplicated devices and inflated counts.
```suggestion
await conn.query(buildCreateDevicesTableSQL());
await conn.insertArrowTable(table as never, { name: "devices" });
```
### Issue 2 of 2
backend/app/routers/telemetry.py:127-133
**Row Header Reports Bytes**
`payload` is the serialized Arrow IPC byte stream, so `len(payload)` reports byte length instead of table rows. Any client or gateway that uses `X-Arrow-Rows` for dataset size will receive incorrect metadata for every Arrow dataset.
```suggestion
telemetry = get_default_telemetry()
table = arrow.telemetry_to_table(telemetry, dataset)
payload = arrow.serialize_ipc(table)
return StreamingResponse(
iter([payload]),
media_type=arrow.ARROW_STREAM_MEDIA_TYPE,
headers={"X-Arrow-Dataset": dataset, "X-Arrow-Rows": str(table.num_rows)},
)
```
Reviews (1): Last reviewed commit: "feat(frontend): native Tailwind 4 CSS gr..." | Re-trigger Greptile |
| await this.init(); | ||
| const conn = this.conn!; | ||
| const table = arrowDevicesToTable(devices); | ||
| await conn.insertArrowTable(table as never, { name: "devices" }); |
There was a problem hiding this comment.
DuckDBAnalytics is mounted only while the analytics view is active, but duckDBAnalytics is a singleton and loadDevices() only inserts into the existing devices table. Switching away and back resets the hook's loadedKey and inserts the same Arrow rows again, so filters and aggregates return duplicated devices and inflated counts.
| await conn.insertArrowTable(table as never, { name: "devices" }); | |
| await conn.query(buildCreateDevicesTableSQL()); | |
| await conn.insertArrowTable(table as never, { name: "devices" }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/duckdbClient.ts
Line: 65
Comment:
**DuckDB Rows Append On Remount**
`DuckDBAnalytics` is mounted only while the analytics view is active, but `duckDBAnalytics` is a singleton and `loadDevices()` only inserts into the existing `devices` table. Switching away and back resets the hook's `loadedKey` and inserts the same Arrow rows again, so filters and aggregates return duplicated devices and inflated counts.
```suggestion
await conn.query(buildCreateDevicesTableSQL());
await conn.insertArrowTable(table as never, { name: "devices" });
```
How can I resolve this? If you propose a fix, please make it concise.| telemetry = get_default_telemetry() | ||
| payload = arrow.serialize_dataset(telemetry, dataset) | ||
| return StreamingResponse( | ||
| iter([payload]), | ||
| media_type=arrow.ARROW_STREAM_MEDIA_TYPE, | ||
| headers={"X-Arrow-Dataset": dataset, "X-Arrow-Rows": str(len(payload))}, | ||
| ) |
There was a problem hiding this comment.
payload is the serialized Arrow IPC byte stream, so len(payload) reports byte length instead of table rows. Any client or gateway that uses X-Arrow-Rows for dataset size will receive incorrect metadata for every Arrow dataset.
| telemetry = get_default_telemetry() | |
| payload = arrow.serialize_dataset(telemetry, dataset) | |
| return StreamingResponse( | |
| iter([payload]), | |
| media_type=arrow.ARROW_STREAM_MEDIA_TYPE, | |
| headers={"X-Arrow-Dataset": dataset, "X-Arrow-Rows": str(len(payload))}, | |
| ) | |
| telemetry = get_default_telemetry() | |
| table = arrow.telemetry_to_table(telemetry, dataset) | |
| payload = arrow.serialize_ipc(table) | |
| return StreamingResponse( | |
| iter([payload]), | |
| media_type=arrow.ARROW_STREAM_MEDIA_TYPE, | |
| headers={"X-Arrow-Dataset": dataset, "X-Arrow-Rows": str(table.num_rows)}, | |
| ) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/app/routers/telemetry.py
Line: 127-133
Comment:
**Row Header Reports Bytes**
`payload` is the serialized Arrow IPC byte stream, so `len(payload)` reports byte length instead of table rows. Any client or gateway that uses `X-Arrow-Rows` for dataset size will receive incorrect metadata for every Arrow dataset.
```suggestion
telemetry = get_default_telemetry()
table = arrow.telemetry_to_table(telemetry, dataset)
payload = arrow.serialize_ipc(table)
return StreamingResponse(
iter([payload]),
media_type=arrow.ARROW_STREAM_MEDIA_TYPE,
headers={"X-Arrow-Dataset": dataset, "X-Arrow-Rows": str(table.num_rows)},
)
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Code Review
This pull request introduces an asynchronous prompt-injection guardrail middleware, zero-copy columnar telemetry streaming using Apache Arrow IPC, and in-browser edge analytics powered by DuckDB-WASM and ONNX-based hybrid RAG. Feedback on these changes highlights several issues: the X-Arrow-Rows header incorrectly reports payload bytes instead of row count; the ONNXEmbedder fails to initialize properly and uses incorrect tensor types; httpx.AsyncClient is inefficiently instantiated on every request; the Arrow Flight server hardcodes an unroutable 0.0.0.0 address; and column lookups in the frontend Arrow client are performed inefficiently inside a loop.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| telemetry = get_default_telemetry() | ||
| payload = arrow.serialize_dataset(telemetry, dataset) | ||
| return StreamingResponse( | ||
| iter([payload]), | ||
| media_type=arrow.ARROW_STREAM_MEDIA_TYPE, | ||
| headers={"X-Arrow-Dataset": dataset, "X-Arrow-Rows": str(len(payload))}, | ||
| ) |
There was a problem hiding this comment.
The X-Arrow-Rows header is currently set to str(len(payload)), which represents the serialized payload size in bytes rather than the actual number of rows in the dataset. This is highly misleading for clients. Retrieve the row count from the PyArrow table before serialization.
| telemetry = get_default_telemetry() | |
| payload = arrow.serialize_dataset(telemetry, dataset) | |
| return StreamingResponse( | |
| iter([payload]), | |
| media_type=arrow.ARROW_STREAM_MEDIA_TYPE, | |
| headers={"X-Arrow-Dataset": dataset, "X-Arrow-Rows": str(len(payload))}, | |
| ) | |
| telemetry = get_default_telemetry() | |
| table = arrow.telemetry_to_table(telemetry, dataset) | |
| payload = arrow.serialize_ipc(table) | |
| return StreamingResponse( | |
| iter([payload]), | |
| media_type=arrow.ARROW_STREAM_MEDIA_TYPE, | |
| headers={"X-Arrow-Dataset": dataset, "X-Arrow-Rows": str(table.num_rows)}, | |
| ) |
| export class ONNXEmbedder { | ||
| private session: unknown | null = null; | ||
| private loading: Promise<void> | null = null; | ||
| private readonly modelUrl?: string; | ||
| private readonly dim: number; | ||
|
|
||
| constructor(opts: { modelUrl?: string; dim?: number } = {}) { | ||
| this.modelUrl = opts.modelUrl; | ||
| this.dim = opts.dim ?? DEFAULT_DIM; | ||
| } | ||
|
|
||
| async ready(): Promise<boolean> { | ||
| if (!this.modelUrl) return false; | ||
| if (this.session) return true; | ||
| if (!this.loading) { | ||
| this.loading = this._load(); | ||
| } | ||
| try { | ||
| await this.loading; | ||
| } catch { | ||
| this.loading = null; | ||
| return false; | ||
| } | ||
| return this.session !== null; | ||
| } | ||
|
|
||
| private async _load(): Promise<void> { | ||
| // Dynamic import keeps onnxruntime-web out of the main bundle until used. | ||
| const ort = await import("onnxruntime-web"); | ||
| const session = await ort.InferenceSession.create(this.modelUrl!, { | ||
| executionProviders: ["wasm"], | ||
| }); | ||
| this.session = session; | ||
| } | ||
|
|
||
| /** Embed one or more texts. Uses ONNX when available, else hashing. */ | ||
| async embed(texts: string[]): Promise<number[][]> { | ||
| const session = this.session as | ||
| | { inputNames: string[]; outputNames: string[]; run: (f: Record<string, unknown>) => Promise<Record<string, { data: Float32Array }>> } | ||
| | null; | ||
|
|
||
| if (!session) { | ||
| return texts.map((t) => hashEmbedding(t, this.dim)); | ||
| } | ||
|
|
||
| const inputName = session.inputNames[0]; | ||
| const results: number[][] = []; | ||
| for (const text of texts) { | ||
| const vec = hashEmbedding(text, this.dim); | ||
| const tensor = makeTensor(vec); | ||
| const out = await session.run({ [inputName]: tensor }); | ||
| const data = out[session.outputNames[0]].data; | ||
| results.push(Array.from(data as Float32Array)); | ||
| } | ||
| return results; | ||
| } | ||
| } | ||
|
|
||
| // Minimal tensor shim so we don't hard-depend on the onnxruntime-web types. | ||
| function makeTensor(vec: number[]) { | ||
| return { | ||
| data: Float32Array.from(vec), | ||
| dims: [1, vec.length], | ||
| type: "float32", | ||
| }; | ||
| } |
There was a problem hiding this comment.
There are two critical bugs in ONNXEmbedder:
- The dynamic import of onnxruntime-web and session creation is only triggered inside ready(), which is never called. Consequently, this.session remains null and the embedder silently falls back to hashEmbedding forever.
- The makeTensor helper returns a plain object, but onnxruntime-web strictly requires real ort.Tensor instances, which would cause a runtime crash once loaded.
We can fix both by lazily calling ready() on the first embed() call and using the real ort.Tensor constructor.
export class ONNXEmbedder {
private session: any | null = null;
private ort: any | null = null;
private loading: Promise<void> | null = null;
private readonly modelUrl?: string;
private readonly dim: number;
constructor(opts: { modelUrl?: string; dim?: number } = {}) {
this.modelUrl = opts.modelUrl;
this.dim = opts.dim ?? DEFAULT_DIM;
}
async ready(): Promise<boolean> {
if (!this.modelUrl) return false;
if (this.session) return true;
if (!this.loading) {
this.loading = this._load();
}
try {
await this.loading;
} catch {
this.loading = null;
return false;
}
return this.session !== null;
}
private async _load(): Promise<void> {
// Dynamic import keeps onnxruntime-web out of the main bundle until used.
const ort = await import("onnxruntime-web");
const session = await ort.InferenceSession.create(this.modelUrl!, {
executionProviders: ["wasm"],
});
this.session = session;
this.ort = ort;
}
/** Embed one or more texts. Uses ONNX when available, else hashing. */
async embed(texts: string[]): Promise<number[][]> {
if (this.modelUrl && !this.session) {
await this.ready();
}
if (!this.session || !this.ort) {
return texts.map((t) => hashEmbedding(t, this.dim));
}
const inputName = this.session.inputNames[0];
const results: number[][] = [];
for (const text of texts) {
const vec = hashEmbedding(text, this.dim);
const tensor = new this.ort.Tensor("float32", Float32Array.from(vec), [1, vec.length]);
const out = await this.session.run({ [inputName]: tensor });
const data = out[this.session.outputNames[0]].data;
results.push(Array.from(data as Float32Array));
}
return results;
}
}| async def _check_edge(self, prompt: str) -> bool: # pragma: no cover - needs live edge svc | ||
| try: | ||
| import httpx | ||
|
|
||
| async with httpx.AsyncClient(timeout=1.0) as client: | ||
| resp = await client.post(self.edge_url, json={"prompt": prompt}) | ||
| if resp.status_code == 200: | ||
| data = resp.json() | ||
| return bool(data.get("allowed", True)) | ||
| # Non-200 from the edge => treat as blocked for safety. | ||
| return False | ||
| except Exception as exc: | ||
| logger.warning( | ||
| "Edge guardrail unreachable (%s); degrading to heuristic", exc | ||
| ) | ||
| result = await asyncio.to_thread(self.guardrails.check_input, prompt) | ||
| return result.allowed |
There was a problem hiding this comment.
Instantiating a new httpx.AsyncClient on every request is highly inefficient because it does not reuse connection pools, leading to socket exhaustion and increased latency under load. Consider lazily initializing a single client instance on the middleware and reusing it across requests.
| async def _check_edge(self, prompt: str) -> bool: # pragma: no cover - needs live edge svc | |
| try: | |
| import httpx | |
| async with httpx.AsyncClient(timeout=1.0) as client: | |
| resp = await client.post(self.edge_url, json={"prompt": prompt}) | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| return bool(data.get("allowed", True)) | |
| # Non-200 from the edge => treat as blocked for safety. | |
| return False | |
| except Exception as exc: | |
| logger.warning( | |
| "Edge guardrail unreachable (%s); degrading to heuristic", exc | |
| ) | |
| result = await asyncio.to_thread(self.guardrails.check_input, prompt) | |
| return result.allowed | |
| async def _check_edge(self, prompt: str) -> bool: # pragma: no cover - needs live edge svc | |
| try: | |
| import httpx | |
| if not hasattr(self, "_client"): | |
| self._client = httpx.AsyncClient(timeout=1.0) | |
| resp = await self._client.post(self.edge_url, json={"prompt": prompt}) | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| return bool(data.get("allowed", True)) | |
| # Non-200 from the edge => treat as blocked for safety. | |
| return False | |
| except Exception as exc: | |
| logger.warning( | |
| "Edge guardrail unreachable (%s); degrading to heuristic", exc | |
| ) | |
| result = await asyncio.to_thread(self.guardrails.check_input, prompt) | |
| return result.allowed |
| def __init__( | ||
| self, | ||
| uri: str = "grpc://0.0.0.0:8815", | ||
| telemetry_provider: Optional[Callable[[], Dict[str, Any]]] = None, | ||
| ) -> None: | ||
| if not FLIGHT_AVAILABLE: | ||
| raise RuntimeError("pyarrow.flight is not available in this build") | ||
| self._uri = uri | ||
| self._telemetry_provider = telemetry_provider or (lambda: {}) |
There was a problem hiding this comment.
Hardcoding 0.0.0.0 as the location in FlightEndpoint is problematic because 0.0.0.0 is not a routable address for clients (especially on non-Unix platforms like Windows). Parse the host and port from the configured uri and fallback to localhost if the host is 0.0.0.0.
def __init__(
self,
uri: str = "grpc://0.0.0.0:8815",
telemetry_provider: Optional[Callable[[], Dict[str, Any]]] = None,
) -> None:
if not FLIGHT_AVAILABLE:
raise RuntimeError("pyarrow.flight is not available in this build")
self._uri = uri
self._telemetry_provider = telemetry_provider or (lambda: {})
from urllib.parse import urlparse
parsed = urlparse(uri)
self._host = parsed.hostname or "localhost"
self._port = parsed.port or 8815
if self._host == "0.0.0.0":
self._host = "localhost"| def _build_server(self) -> "_flight.FlightServerBase": | ||
| provider = self._telemetry_provider | ||
|
|
||
| class _Server(_flight.FlightServerBase): | ||
| def list_flights(self, context, criteria): # noqa: D401, ANN001 | ||
| for ds in DATASETS: | ||
| yield _flight.FlightInfo( | ||
| telemetry_to_table(provider(), ds).schema, | ||
| _flight.FlightDescriptor.for_command(ds.encode()), | ||
| [], | ||
| -1, | ||
| -1, | ||
| ) | ||
|
|
||
| def get_flight_info(self, context, descriptor): # noqa: ANN001 | ||
| ds = descriptor.command.decode() if descriptor.command else "devices" | ||
| table = telemetry_to_table(provider(), ds) | ||
| endpoint = _flight.FlightEndpoint( | ||
| ds.encode(), | ||
| [_flight.Location.for_grpc_tcp("0.0.0.0", 8815)], | ||
| ) | ||
| return _flight.FlightInfo( | ||
| table.schema, descriptor, [endpoint], table.num_rows, -1 | ||
| ) | ||
|
|
||
| def do_get(self, context, ticket): # noqa: ANN001 | ||
| ds = ticket.ticket.decode() | ||
| table = telemetry_to_table(provider(), ds) | ||
| return _flight.RecordBatchStream(table) | ||
|
|
||
| def do_put(self, context, descriptor, reader): # noqa: ANN001 | ||
| # Read-and-discard: this server is a read-only publisher. | ||
| for _ in reader: | ||
| pass | ||
|
|
||
| return _Server(self._uri) |
There was a problem hiding this comment.
Update the Flight server builder to use the parsed host and port instead of hardcoded "0.0.0.0" and 8815 so that clients receive a valid, routable endpoint location.
def _build_server(self) -> "_flight.FlightServerBase":
provider = self._telemetry_provider
host = self._host
port = self._port
class _Server(_flight.FlightServerBase):
def list_flights(self, context, criteria): # noqa: D401, ANN001
for ds in DATASETS:
yield _flight.FlightInfo(
telemetry_to_table(provider(), ds).schema,
_flight.FlightDescriptor.for_command(ds.encode()),
[],
-1,
-1,
)
def get_flight_info(self, context, descriptor): # noqa: ANN001
ds = descriptor.command.decode() if descriptor.command else "devices"
table = telemetry_to_table(provider(), ds)
endpoint = _flight.FlightEndpoint(
ds.encode(),
[_flight.Location.for_grpc_tcp(host, port)],
)
return _flight.FlightInfo(
table.schema, descriptor, [endpoint], table.num_rows, -1
)
def do_get(self, context, ticket): # noqa: ANN001
ds = ticket.ticket.decode()
table = telemetry_to_table(provider(), ds)
return _flight.RecordBatchStream(table)
def do_put(self, context, descriptor, reader): # noqa: ANN001
# Read-and-discard: this server is a read-only publisher.
for _ in reader:
pass
return _Server(self._uri)| export function tableToDevices(table: Table): ArrowDevice[] { | ||
| const cols = table.schema.fields.map((f) => f.name); | ||
| const col = (name: string) => table.getChild(name); | ||
| const n = table.numRows; | ||
| const out: ArrowDevice[] = []; | ||
|
|
||
| for (let i = 0; i < n; i++) { | ||
| const at = (name: string) => col(name)?.get(i); | ||
| out.push({ | ||
| id: String(at("id") ?? ""), | ||
| score: Number(at("score") ?? 0), | ||
| uptime: Number(at("uptime") ?? 0), | ||
| load: String(at("load") ?? ""), | ||
| status: String(at("status") ?? ""), | ||
| type: String(at("type") ?? ""), | ||
| active: Boolean(at("active") ?? false), | ||
| }); | ||
| } | ||
| void cols; | ||
| return out; | ||
| } | ||
|
|
There was a problem hiding this comment.
Looking up columns by name inside the loop via col(name) is highly inefficient as it performs a lookup O(N * M) times. Extract the column references once outside the loop to improve performance significantly.
/** Convert a `devices` Arrow table into structured device records. */
export function tableToDevices(table: Table): ArrowDevice[] {
const idCol = table.getChild("id");
const scoreCol = table.getChild("score");
const uptimeCol = table.getChild("uptime");
const loadCol = table.getChild("load");
const statusCol = table.getChild("status");
const typeCol = table.getChild("type");
const activeCol = table.getChild("active");
const n = table.numRows;
const out: ArrowDevice[] = [];
for (let i = 0; i < n; i++) {
out.push({
id: String(idCol?.get(i) ?? ""),
score: Number(scoreCol?.get(i) ?? 0),
uptime: Number(uptimeCol?.get(i) ?? 0),
load: String(loadCol?.get(i) ?? ""),
status: String(statusCol?.get(i) ?? ""),
type: String(typeCol?.get(i) ?? ""),
active: Boolean(activeCol?.get(i) ?? false),
});
}
return out;
}
No description provided.