• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 26033117099

18 May 2026 12:20PM UTC coverage: 74.769% (-0.1%) from 74.887%
26033117099

push

github

web-flow
feat(arrow) Streaming ArrowTextLayer (#2620)

6882 of 10396 branches covered (66.2%)

Branch coverage included in aggregate %.

194 of 250 new or added lines in 9 files covered. (77.6%)

13 existing lines in 7 files now uncovered.

15038 of 18921 relevant lines covered (79.48%)

914.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.13
/modules/arrow/src/arrow/arrow-gpu-record-batch.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {Buffer, Device, type BufferLayout, type ShaderLayout} from '@luma.gl/core';
6
import type {DynamicBuffer} from '@luma.gl/engine';
7
import * as arrow from 'apache-arrow';
8
import {getArrowFieldByPath, getArrowVectorByPath} from './arrow-paths';
9
import {getArrowBufferLayout, type ArrowVertexFormatOptions} from './arrow-shader-layout';
10
import type {AttributeArrowType} from './arrow-types';
11
import {
12
  getAppendableGPUColumnData,
13
  getAppendableGPUColumns,
14
  type AppendableGPUColumn
15
} from './arrow-gpu-appendable';
16
import {
17
  GPUVector,
18
  type GPUVectorBufferProps,
19
  type GPUVectorDynamicBufferProps,
20
  type GPUVectorProps
21
} from './arrow-gpu-vector';
22
import {createGPUVectorCollection} from './arrow-gpu-vector-collection';
23

24
/** Options for creating GPU buffers from shader-compatible Arrow record batch columns. */
25
export type GPURecordBatchProps = ArrowVertexFormatOptions & {
26
  /** Shader layout that selects which Arrow columns should be uploaded. */
27
  shaderLayout: ShaderLayout;
28
  /** Maps shader attribute names to Arrow column paths. Defaults to using the attribute name. */
29
  arrowPaths?: Record<string, string>;
30
  /** Buffer props applied to every Arrow-backed GPU vector. */
31
  bufferProps?: GPUVectorProps;
32
};
33

34
/** Options for constructing an GPURecordBatch from existing GPU vectors. */
35
export type GPURecordBatchFromVectorsProps = {
36
  /** GPU vectors keyed by name, or a list of named GPU vectors. */
37
  vectors: Record<string, GPUVector> | GPUVector[];
38
  /** Optional precomputed batch buffer layouts. */
39
  bufferLayout?: BufferLayout[];
40
  /** Optional selected schema fields. Defaults to fields synthesized from vector names and types. */
41
  fields?: arrow.Field[];
42
  /** Optional batch-level schema metadata. */
43
  metadata?: Map<string, string>;
44
  /** Number of null rows in the generated GPU record batch. */
45
  nullCount?: number;
46
};
47

48
/** Props for constructing an empty appendable GPU record batch from a selected schema. */
49
export type GPURecordBatchAppendableProps = ArrowVertexFormatOptions & {
50
  /** Discriminator for appendable record batch construction. */
51
  type: 'appendable';
52
  /** Device that creates appendable vector storage. */
53
  device: Device;
54
  /** Source schema used to select shader-compatible columns. */
55
  schema: arrow.Schema;
56
  /** Shader layout that selects which Arrow columns should be uploaded. */
57
  shaderLayout: ShaderLayout;
58
  /** Maps shader attribute names to Arrow column paths. Defaults to using the attribute name. */
59
  arrowPaths?: Record<string, string>;
60
  /** Initial row capacity for each appendable vector. */
61
  initialCapacityRows?: number;
62
  /** Appendable vector capacity growth multiplier. */
63
  capacityGrowthFactor?: number;
64
  /** Dynamic buffer props forwarded to appendable vectors. */
65
  bufferProps?: GPUVectorDynamicBufferProps;
66
};
67

68
/** GPU memory and Arrow schema metadata for one selected Arrow RecordBatch. */
69
export class GPURecordBatch {
70
  /** GPU-facing schema for the selected shader attribute columns. */
71
  schema: arrow.Schema;
72
  /** Number of rows in the source Arrow record batch. */
73
  numRows: number;
74
  /** Number of selected GPU columns in {@link schema}. */
75
  numCols: number;
76
  /** Number of null rows in the source Arrow record batch. */
77
  nullCount: number;
78
  /** Buffer layout derived from the selected Arrow columns and shader layout. */
79
  readonly bufferLayout: BufferLayout[] = [];
55✔
80
  /** GPU vectors keyed by shader attribute name. */
81
  readonly gpuVectors: Record<string, GPUVector> = {};
55✔
82
  /** Model-ready attribute buffers keyed by shader attribute name. */
83
  readonly attributes: Record<string, Buffer | DynamicBuffer> = {};
55✔
84
  /** Model-ready storage bindings keyed by shader binding name. */
85
  readonly bindings: Record<string, Buffer | DynamicBuffer> = {};
55✔
86
  private readonly appendableColumns?: AppendableGPUColumn[];
87

88
  /** Creates GPU buffers and GPU-facing schema from one Arrow record batch. */
89
  constructor(device: Device, recordBatch: arrow.RecordBatch, props: GPURecordBatchProps);
90
  /** Creates a GPU-facing record batch from existing named GPU vectors. */
91
  constructor(props: GPURecordBatchFromVectorsProps);
92
  /** Creates an empty appendable GPU record batch from a selected schema. */
93
  constructor(props: GPURecordBatchAppendableProps);
94
  constructor(
95
    deviceOrProps: Device | GPURecordBatchFromVectorsProps | GPURecordBatchAppendableProps,
96
    recordBatch?: arrow.RecordBatch,
97
    props?: GPURecordBatchProps
98
  ) {
99
    if (!(deviceOrProps instanceof Device)) {
55✔
100
      if ('type' in deviceOrProps && deviceOrProps.type === 'appendable') {
10✔
101
        const options = deviceOrProps;
3✔
102
        const appendableColumns = getAppendableGPUColumns({
3✔
103
          schema: options.schema,
104
          shaderLayout: options.shaderLayout,
105
          arrowPaths: options.arrowPaths,
106
          allowWebGLOnlyFormats: options.allowWebGLOnlyFormats
107
        });
108
        this.numRows = 0;
3✔
109
        this.nullCount = 0;
3✔
110
        this.bufferLayout.push(
3✔
111
          ...appendableColumns.flatMap(column => (column.bufferLayout ? [column.bufferLayout] : []))
6✔
112
        );
113
        this.appendableColumns = appendableColumns;
3✔
114

115
        const fields: arrow.Field[] = [];
3✔
116
        for (const column of appendableColumns) {
3✔
117
          const {attributeName, bufferLayout, field: sourceField} = column;
6✔
118
          const field = new arrow.Field(
6✔
119
            attributeName,
120
            sourceField.type,
121
            sourceField.nullable,
122
            new Map(sourceField.metadata)
123
          );
124
          const gpuVector = new GPUVector({
6✔
125
            type: 'appendable',
126
            name: attributeName,
127
            device: options.device,
128
            arrowType: sourceField.type as AttributeArrowType | arrow.Utf8,
129
            initialCapacityRows: options.initialCapacityRows,
130
            capacityGrowthFactor: options.capacityGrowthFactor,
131
            bufferProps: options.bufferProps
132
          } as any);
133

134
          fields.push(field);
6✔
135
          this.gpuVectors[attributeName] = gpuVector;
6✔
136
          if (bufferLayout) {
6✔
137
            this.attributes[attributeName] = gpuVector.buffer;
5✔
138
          } else {
139
            this.bindings[attributeName] = gpuVector.buffer;
1✔
140
          }
141
        }
142

143
        this.schema = new arrow.Schema(fields, new Map(options.schema.metadata));
3✔
144
        this.numCols = fields.length;
3✔
145
        return;
3✔
146
      }
147

148
      const {vectors, bufferLayout, fields, metadata, nullCount = 0} = deviceOrProps;
7✔
149
      const vectorCollection = createGPUVectorCollection({
7✔
150
        ownerName: 'GPURecordBatch',
151
        vectors,
152
        bufferLayout,
153
        fields
154
      });
155

156
      this.numRows = vectorCollection.numRows;
7✔
157
      this.nullCount = nullCount;
7✔
158
      Object.assign(this.gpuVectors, vectorCollection.gpuVectors);
7✔
159
      Object.assign(this.attributes, vectorCollection.attributes);
7✔
160
      this.bufferLayout.push(...vectorCollection.bufferLayout);
7✔
161

162
      this.schema = new arrow.Schema(vectorCollection.fields, metadata);
7✔
163
      this.numCols = vectorCollection.fields.length;
7✔
164
      return;
7✔
165
    }
166

167
    const device = deviceOrProps;
45✔
168
    const batch = recordBatch!;
45✔
169
    const options = props!;
45✔
170
    const table = new arrow.Table([batch]);
45✔
171

172
    this.numRows = batch.numRows;
45✔
173
    this.nullCount = batch.nullCount;
45✔
174
    this.bufferLayout = getArrowBufferLayout(options.shaderLayout, {
45✔
175
      arrowTable: table,
176
      arrowPaths: options.arrowPaths,
177
      allowWebGLOnlyFormats: options.allowWebGLOnlyFormats
178
    });
179

180
    const fields: arrow.Field[] = [];
45✔
181
    const selectedNames = new Set<string>();
45✔
182
    for (const bufferLayout of this.bufferLayout) {
45✔
183
      const arrowPath = options.arrowPaths?.[bufferLayout.name] || bufferLayout.name;
69✔
184
      const vector = getArrowVectorByPath(table, arrowPath);
69✔
185
      const sourceField = getArrowFieldByPath(table, arrowPath);
69✔
186
      const field = new arrow.Field(
69✔
187
        bufferLayout.name,
188
        vector.type,
189
        sourceField.nullable,
190
        new Map(sourceField.metadata)
191
      );
192
      const gpuVector = new GPUVector(
69✔
193
        device,
194
        vector as arrow.Vector<AttributeArrowType>,
195
        options.bufferProps as GPUVectorBufferProps
196
      );
197

198
      fields.push(field);
69✔
199
      selectedNames.add(bufferLayout.name);
69✔
200
      this.gpuVectors[bufferLayout.name] = gpuVector;
69✔
201
      if (bufferLayout.attributes) {
69!
202
        for (const attribute of bufferLayout.attributes) {
×
203
          this.attributes[attribute.attribute] = gpuVector.buffer;
×
204
        }
205
      } else {
206
        this.attributes[bufferLayout.name] = gpuVector.buffer;
69✔
207
      }
208
    }
209

210
    for (const storageBinding of getArrowStorageBindings(options.shaderLayout)) {
45✔
211
      if (selectedNames.has(storageBinding.name)) {
8!
212
        throw new Error(
×
213
          `GPURecordBatch shader input "${storageBinding.name}" cannot be both an attribute and a storage binding`
214
        );
215
      }
216
      const arrowPath = options.arrowPaths?.[storageBinding.name] || storageBinding.name;
8✔
217
      const vector = tryGetArrowVectorByPath(table, arrowPath);
8✔
218
      const sourceField = tryGetArrowFieldByPath(table, arrowPath);
8✔
219
      if (!vector || !sourceField) {
8!
220
        continue;
×
221
      }
222
      const gpuVector = new GPUVector(
8✔
223
        device,
224
        vector as arrow.Vector<AttributeArrowType>,
225
        options.bufferProps as GPUVectorBufferProps
226
      );
227
      const field = new arrow.Field(
8✔
228
        storageBinding.name,
229
        vector.type,
230
        sourceField.nullable,
231
        new Map(sourceField.metadata)
232
      );
233

234
      fields.push(field);
8✔
235
      selectedNames.add(storageBinding.name);
8✔
236
      this.gpuVectors[storageBinding.name] = gpuVector;
8✔
237
      this.bindings[storageBinding.name] = getSingleGPUVectorDataBuffer(
8✔
238
        gpuVector,
239
        storageBinding.name
240
      );
241
    }
242

243
    this.schema = new arrow.Schema(fields, new Map(batch.schema.metadata));
45✔
244
    this.numCols = fields.length;
45✔
245
  }
246

247
  /** Appends one Arrow record batch into this appendable GPU record batch. */
248
  addToLastBatch(recordBatch: arrow.RecordBatch): this {
249
    if (!this.appendableColumns) {
5!
250
      throw new Error('GPURecordBatch.addToLastBatch() requires appendable batch storage');
×
251
    }
252
    const pendingData = getAppendableGPUColumnData(
5✔
253
      recordBatch,
254
      this.appendableColumns,
255
      'GPURecordBatch.addToLastBatch()'
256
    );
257
    for (const {column, data} of pendingData) {
5✔
258
      const vector = this.gpuVectors[column.attributeName];
10✔
259
      vector.addToLastData(data as any);
10✔
260
    }
261
    this.numRows += recordBatch.numRows;
5✔
262
    this.nullCount += recordBatch.nullCount;
5✔
263
    return this;
5✔
264
  }
265

266
  /** Clears appendable vector rows while retaining reusable DynamicBuffer allocations. */
267
  resetLastBatch(): this {
268
    if (!this.appendableColumns) {
1!
269
      throw new Error('GPURecordBatch.resetLastBatch() requires appendable batch storage');
×
270
    }
271
    for (const vector of Object.values(this.gpuVectors)) {
1✔
272
      vector.resetLastBatch();
2✔
273
    }
274
    this.numRows = 0;
1✔
275
    this.nullCount = 0;
1✔
276
    return this;
1✔
277
  }
278

279
  destroy(): void {
280
    for (const gpuVector of Object.values(this.gpuVectors)) {
55✔
281
      gpuVector.destroy();
91✔
282
    }
283
  }
284
}
285

286
function getSingleGPUVectorDataBuffer(vector: GPUVector, bindingName: string): DynamicBuffer {
287
  const [data, ...remainingData] = vector.data;
8✔
288
  if (!data || remainingData.length > 0) {
8!
NEW
289
    throw new Error(
×
290
      `GPURecordBatch storage binding "${bindingName}" requires exactly one GPUData chunk`
291
    );
292
  }
293
  return data.buffer;
8✔
294
}
295

296
function getArrowStorageBindings(shaderLayout: ShaderLayout): Array<{name: string}> {
297
  return shaderLayout.bindings.filter(
45✔
298
    binding =>
8✔
299
      (binding.type === 'storage' || binding.type === 'read-only-storage') && !('format' in binding)
300
  );
301
}
302

303
function tryGetArrowVectorByPath(table: arrow.Table, path: string): arrow.Vector | null {
304
  try {
8✔
305
    return getArrowVectorByPath(table, path);
8✔
306
  } catch {
307
    return null;
×
308
  }
309
}
310

311
function tryGetArrowFieldByPath(table: arrow.Table, path: string): arrow.Field | null {
312
  try {
8✔
313
    return getArrowFieldByPath(table, path);
8✔
314
  } catch {
315
    return null;
×
316
  }
317
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc