@malloydata/render 0.0.422 → 0.0.424

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ # Field Creation Analysis: Excessive Field Creation and Tag Parsing in malloy-render
2
+
3
+ ## Problem Statement
4
+
5
+ In the malloy-render package, we're experiencing significant performance overhead during data visualization preprocessing. For a simple two-column Malloy query like:
6
+
7
+ ```malloy
8
+ view: two_column is {
9
+ select: brand, department
10
+ limit: 1
11
+ }
12
+ ```
13
+
14
+ We expect to see:
15
+ - 2 Field objects created (one for each column)
16
+ - 2 tagFor calls (one for each field's metadata parsing)
17
+
18
+ However, we're actually seeing:
19
+ - **5 Field objects created** (2.5x expected)
20
+ - **7-8 tagFor calls** (3.5-4x expected)
21
+
22
+ Since tag parsing is an expensive operation (parsing annotations and metadata), this overhead becomes significant with large datasets. For example, with 5000 rows containing nested data, this can lead to hundreds of thousands of unnecessary field creations and tag parsing operations.
23
+
24
+ ## Root Cause Analysis
25
+
26
+ ### The Problem Flow
27
+
28
+ 1. **RootField Creation** (extends RepeatedRecordField)
29
+ - Creates RootField instance → 1 tagFor call for RootField itself
30
+
31
+ 2. **ArrayField Constructor** (line 27 in nest.ts)
32
+ - Creates an `elementField` for the array's element type
33
+ - This creates a RecordField named "element" → 1 Field.from call + 1 tagFor call
34
+
35
+ 3. **RecordField Constructor** (line 159 in nest.ts)
36
+ - Creates fields for brand and department as children of "element"
37
+ - 2 Field.from calls + 2 tagFor calls
38
+
39
+ 4. **RepeatedRecordField Constructor** (line 54 in nest.ts)
40
+ - **DUPLICATES** the field creation for brand and department
41
+ - Creates them again as direct children of RepeatedRecordField
42
+ - 2 more Field.from calls + 2 more tagFor calls
43
+
44
+ 5. **Additional RecordField Creation** (lines 58-71 in nest.ts)
45
+ - Creates a `nestedRecordField` instance
46
+ - 1 more tagFor call
47
+
48
+ ### The Duplication Issue
49
+
50
+ The main issue is that **RepeatedRecordField creates fields TWICE**:
51
+
52
+ 1. First in ArrayField's constructor via `elementField`
53
+ 2. Then again in RepeatedRecordField's own constructor at line 54
54
+
55
+ This happens because:
56
+ - ArrayField creates an elementField (RecordField) which creates brand/department fields
57
+ - RepeatedRecordField then directly parses the record type and creates brand/department fields again
58
+ - The `nestedRecordField` creation adds another layer of overhead
59
+
60
+ ## Current Architecture Analysis
61
+
62
+ ### Why the Extra Fields Exist
63
+
64
+ 1. **ArrayField's `elementField`**
65
+ - **Purpose**: Provides schema for array elements (could be any type: string, number, record, etc.)
66
+ - **Used by**: ArrayCell to create appropriate cell types for each array element
67
+ - **Necessity**: Required for generic array handling
68
+
69
+ 2. **RepeatedRecordField's Triple Structure**
70
+ - **`elementField`** (inherited from ArrayField): Generic element schema
71
+ - **`fields` array**: Direct access to record fields for performance
72
+ - **`nestedRecordField`**: RecordField instance for cell creation compatibility
73
+
74
+ 3. **The Architecture's Intent**
75
+
76
+ The design appears to be intentional for several reasons:
77
+ - **Type Safety**: Each field type knows exactly what kind of data it contains
78
+ - **Performance**: Direct field access via `fields` array avoids indirection
79
+ - **Compatibility**: The `nestedRecordField` allows RepeatedRecordField to work seamlessly with RecordCell
80
+
81
+ ### Why This Architecture Evolved
82
+
83
+ 1. **Type System Alignment**: Mirrors Malloy's type system where arrays and records are distinct
84
+ 2. **Performance Optimization**: Direct field access without indirection
85
+ 3. **API Compatibility**: RecordCell expects a RecordField, not a RepeatedRecordField
86
+ 4. **Incremental Development**: Features added over time without refactoring base structures
87
+
88
+ ## Rearchitecture Proposals
89
+
90
+ ### Option 1: Lazy Field Creation (Minimal Change)
91
+ **Concept**: Create fields only when accessed, cache aggressively
92
+
93
+ **Pros**:
94
+ - Minimal API changes
95
+ - Reduces upfront cost
96
+ - Fields created only once
97
+
98
+ **Cons**:
99
+ - Doesn't eliminate architectural duplication
100
+ - Just defers the problem
101
+
102
+ ### Option 2: Unified Field Registry (Medium Change)
103
+ **Concept**: Single source of truth for all fields with automatic deduplication
104
+
105
+ **Pros**:
106
+ - Single point of field creation
107
+ - Automatic deduplication
108
+ - Easy to add metrics/debugging
109
+
110
+ **Cons**:
111
+ - Requires passing registry throughout
112
+ - May complicate field lifecycle
113
+
114
+ ### Option 3: Rethink Inheritance (Major Change)
115
+ **Concept**: RepeatedRecordField shouldn't extend ArrayField since it's conceptually a table, not an array
116
+
117
+ **Pros**:
118
+ - Eliminates conceptual mismatch
119
+ - No duplicate fields
120
+ - Cleaner mental model
121
+
122
+ **Cons**:
123
+ - Breaking API change
124
+ - Significant refactoring needed
125
+
126
+ ### Option 4: Cell-Centric Architecture (Revolutionary)
127
+ **Concept**: Fields become lightweight metadata, cells handle complexity and create child fields on demand
128
+
129
+ **Pros**:
130
+ - Fields created only when data is processed
131
+ - Minimal memory for schema
132
+ - Natural lazy evaluation
133
+
134
+ **Cons**:
135
+ - Major paradigm shift
136
+ - May impact performance differently
137
+
138
+ ## Recommendation
139
+
140
+ **Short term**: Implement the current approach with `skipFieldCreation` flag. This solves the immediate performance issue by:
141
+ - Creating fields once in RepeatedRecordField constructor
142
+ - Sharing these fields with the nestedRecordField
143
+ - Preventing RecordField from creating its own duplicate fields
144
+
145
+ **Long term**: Consider **Option 3 (Rethink Inheritance)**. The fundamental issue is that RepeatedRecordField is conceptually not an array - it's a table. The inheritance from ArrayField creates unnecessary complexity.
146
+
147
+ The ideal architecture would:
148
+ 1. Create each field exactly once
149
+ 2. Share field instances where semantically equivalent
150
+ 3. Align with the mental model of tables vs arrays
151
+ 4. Minimize memory usage for large datasets
152
+
153
+ ## Impact
154
+
155
+ For the example two-column query, fixing this would reduce:
156
+ - Field creation from 5 to 2 (60% reduction)
157
+ - tagFor calls from 7-8 to 2 (71-75% reduction)
158
+
159
+ For larger datasets with nested structures, this optimization becomes even more critical, potentially eliminating hundreds of thousands of unnecessary operations.
160
+
161
+ ## Implementation Status
162
+
163
+ ### Completed (Short-term Fix)
164
+ ✅ **Lazy elementField creation in ArrayField**: The elementField is now created only when accessed, preventing duplicate field creation for RepeatedRecordField instances.
165
+
166
+ ✅ **Field sharing in RepeatedRecordField**: The RepeatedRecordField now shares its fields with both the nestedRecordField and the elementField (when accessed), eliminating duplicate Field.from calls.
167
+
168
+ ✅ **Skip redundant tag parsing**: Added skipTagParsing parameter to prevent unnecessary tagFor calls for the synthetic RecordField instance.
169
+
170
+ **Results achieved**:
171
+ - Field.from calls reduced from 5 to 2 ✓
172
+ - tagFor calls reduced from 7-8 to 4 (further optimization possible)
173
+
174
+ ### Future Work (Long-term Architecture)
175
+ The long-term architectural improvements (Option 3: Rethink Inheritance) remain as future work. This would involve:
176
+ - Redesigning RepeatedRecordField to not extend ArrayField
177
+ - Creating a more appropriate inheritance hierarchy that reflects the conceptual difference between arrays and tables
178
+ - Potentially achieving the ideal 2 tagFor calls by eliminating all redundant metadata parsing
@@ -0,0 +1,238 @@
1
+ # Plugin API Reference
2
+
3
+ ## Core Types
4
+
5
+ ### RenderPluginFactory<TInstance>
6
+
7
+ ```typescript
8
+ interface RenderPluginFactory<TInstance extends RenderPluginInstance> {
9
+ readonly name: string;
10
+ getValidationSpec?(): RendererValidationSpec;
11
+ matches(field: Field, fieldTag: Tag, fieldType: FieldType): boolean;
12
+ create(field: Field, pluginOptions?: unknown, modelTag?: Tag): TInstance;
13
+ }
14
+ ```
15
+
16
+ #### Properties
17
+ - `name`: Unique identifier for the plugin
18
+
19
+ #### Methods
20
+ - `getValidationSpec()`: Declares which tag paths this plugin semantically owns, so the unread-tag detector does not false-warn on valid uses. Required for plugins that read tags at render time. See [validation.md](validation.md) for the ownership model.
21
+
22
+ - `matches()`: Determines if plugin should handle a field
23
+ - `field`: Field metadata and structure
24
+ - `fieldTag`: Field annotations/tags
25
+ - `fieldType`: Enumerated field type
26
+ - Returns: `boolean`
27
+ - Throw when the user's tag asks for this plugin but the field cannot support it — the user sees a red error tile. See [validation.md](validation.md) for the throw-vs-log rule.
28
+
29
+ - `create()`: Instantiates plugin for a matched field
30
+ - `field`: Field to render
31
+ - `pluginOptions`: Configuration from renderer options
32
+ - `modelTag`: Model-level annotations
33
+ - Returns: Plugin instance
34
+ - Do all tag reads here (setup time), not in `renderComponent()` or `beforeRender()` — setup-time reads are validated and marked as consumed automatically.
35
+
36
+ ### RendererValidationSpec
37
+
38
+ ```typescript
39
+ interface RendererValidationSpec {
40
+ readonly renderer: string;
41
+ readonly ownedPaths?: string[][];
42
+ readonly childOwnedPaths?: string[][];
43
+ }
44
+ ```
45
+
46
+ Declares the tag paths a renderer owns — tags whose meaning depends on this renderer being active. See [validation.md](validation.md) for when to declare ownership.
47
+
48
+ ### RenderPluginInstance
49
+
50
+ Base type: Union of `SolidJSRenderPluginInstance` and `DOMRenderPluginInstance`
51
+
52
+ #### Common Properties
53
+ ```typescript
54
+ interface BaseRenderPluginInstance<TMetadata = unknown> {
55
+ readonly name: string;
56
+ readonly field: Field;
57
+ readonly sizingStrategy: 'fixed' | 'fill';
58
+
59
+ getMetadata(): TMetadata;
60
+ processData?(field: NestField, cell: NestCell): void;
61
+ beforeRender?(metadata: RenderMetadata, options: GetResultMetadataOptions): void;
62
+ }
63
+ ```
64
+
65
+ ### SolidJSRenderPluginInstance
66
+
67
+ ```typescript
68
+ interface SolidJSRenderPluginInstance<TMetadata = unknown>
69
+ extends BaseRenderPluginInstance<TMetadata> {
70
+ readonly renderMode: 'solidjs';
71
+ renderComponent(props: RenderProps): JSXElement;
72
+ }
73
+ ```
74
+
75
+ ### DOMRenderPluginInstance
76
+
77
+ ```typescript
78
+ interface DOMRenderPluginInstance<TMetadata = unknown>
79
+ extends BaseRenderPluginInstance<TMetadata> {
80
+ readonly renderMode: 'dom';
81
+ renderToDOM(container: HTMLElement, props: RenderProps): void;
82
+ cleanup?(container: HTMLElement): void;
83
+ }
84
+ ```
85
+
86
+ ### CoreVizPluginInstance
87
+
88
+ ```typescript
89
+ interface CoreVizPluginMethods {
90
+ getSchema(): JSONSchemaObject;
91
+ getSettings(): Record<string, unknown>;
92
+ getDefaultSettings(): Record<string, unknown>;
93
+ settingsToTag(settings: Record<string, unknown>): Tag;
94
+ }
95
+
96
+ type CoreVizPluginInstance<TMetadata = unknown> =
97
+ SolidJSRenderPluginInstance<TMetadata> & CoreVizPluginMethods;
98
+ ```
99
+
100
+ ## Supporting Types
101
+
102
+ ### RenderProps
103
+
104
+ ```typescript
105
+ interface RenderProps {
106
+ dataColumn: Cell;
107
+ field: Field;
108
+ customProps?: Record<string, unknown>;
109
+ }
110
+ ```
111
+
112
+ ### FieldType Enum
113
+
114
+ ```typescript
115
+ enum FieldType {
116
+ String = 'string',
117
+ Number = 'number',
118
+ Boolean = 'boolean',
119
+ Date = 'date',
120
+ Timestamp = 'timestamp',
121
+ JSON = 'json',
122
+ Record = 'record',
123
+ RepeatedRecord = 'repeated_record',
124
+ SQLNative = 'sql_native'
125
+ }
126
+ ```
127
+
128
+ ### Field Interface (Key Methods)
129
+
130
+ ```typescript
131
+ interface Field {
132
+ name: string;
133
+ key: string;
134
+ tag: Tag;
135
+
136
+ isString(): boolean;
137
+ isNumber(): boolean;
138
+ isBoolean(): boolean;
139
+ isDate(): boolean;
140
+ isTime(): boolean;
141
+ isJSON(): boolean;
142
+ isNest(): boolean;
143
+ isBasic(): boolean;
144
+
145
+ // For nested fields
146
+ fields?: Field[];
147
+ fieldAt(path: string): Field | undefined;
148
+ }
149
+ ```
150
+
151
+ ### Cell Interface (Key Methods)
152
+
153
+ ```typescript
154
+ interface Cell {
155
+ value: any;
156
+
157
+ isNull(): boolean;
158
+ isString(): boolean;
159
+ isNumber(): boolean;
160
+ isBoolean(): boolean;
161
+ isDate(): boolean;
162
+ isTime(): boolean;
163
+ isJSON(): boolean;
164
+ isRepeatedRecord(): boolean;
165
+
166
+ // For nested cells
167
+ column(name: string): Cell;
168
+ }
169
+ ```
170
+
171
+ ### Tag Interface (Key Methods)
172
+
173
+ ```typescript
174
+ interface Tag {
175
+ has(key: string): boolean;
176
+ text(key: string): string | undefined;
177
+ boolean(key: string): boolean | undefined;
178
+ number(key: string): number | undefined;
179
+ json(key: string): any | undefined;
180
+ }
181
+ ```
182
+
183
+ ## Renderer Integration
184
+
185
+ ### MalloyRendererOptions
186
+
187
+ ```typescript
188
+ interface MalloyRendererOptions {
189
+ plugins?: RenderPluginFactory[];
190
+ pluginOptions?: Record<string, unknown>;
191
+ // ... other options
192
+ }
193
+ ```
194
+
195
+ ### Plugin Registration
196
+
197
+ ```typescript
198
+ const renderer = new MalloyRenderer({
199
+ plugins: [Plugin1Factory, Plugin2Factory],
200
+ pluginOptions: {
201
+ 'plugin1': { /* options */ },
202
+ 'plugin2': { /* options */ }
203
+ }
204
+ });
205
+ ```
206
+
207
+ ## Utility Functions
208
+
209
+ ### isCoreVizPluginInstance
210
+
211
+ ```typescript
212
+ function isCoreVizPluginInstance(
213
+ plugin: RenderPluginInstance
214
+ ): plugin is CoreVizPluginInstance
215
+ ```
216
+
217
+ Checks if a plugin implements the core visualization interface.
218
+
219
+ ## Lifecycle Methods
220
+
221
+ ### processData(field, cell)
222
+ - Called during data processing phase
223
+ - Use for expensive calculations
224
+ - Results can be stored in plugin instance
225
+
226
+ ### beforeRender(metadata, options)
227
+ - Called before rendering
228
+ - Access to full result metadata
229
+ - Can access Vega config overrides
230
+
231
+ ### renderComponent(props) / renderToDOM(container, props)
232
+ - Called to render the visualization
233
+ - Access to current data cell and field metadata
234
+
235
+ ### cleanup(container)
236
+ - DOM plugins only
237
+ - Called when visualization is removed
238
+ - Clean up event listeners, timers, etc.
@@ -0,0 +1,103 @@
1
+ # Malloy Render Plugin Quick Start
2
+
3
+ ## Minimal Plugin Example
4
+
5
+ ```typescript
6
+ import type { RenderPluginFactory, SolidJSRenderPluginInstance } from '@/api/plugin-types';
7
+
8
+ export const MinimalPluginFactory: RenderPluginFactory<SolidJSRenderPluginInstance> = {
9
+ name: 'minimal',
10
+
11
+ matches: (field, fieldTag) => fieldTag.has('minimal'),
12
+
13
+ create: (field) => ({
14
+ name: 'minimal',
15
+ field,
16
+ renderMode: 'solidjs',
17
+ sizingStrategy: 'fixed',
18
+
19
+ renderComponent: (props) => (
20
+ <div>{props.dataColumn.value}</div>
21
+ ),
22
+
23
+ getMetadata: () => ({ type: 'minimal' })
24
+ })
25
+ };
26
+ ```
27
+
28
+ ## Key Concepts
29
+
30
+ ### 1. Factory Pattern
31
+
32
+ - **Factory** creates plugin instances
33
+ - **Instance** handles rendering
34
+
35
+ ### 2. Matching Fields
36
+
37
+ Plugins match fields based on:
38
+
39
+ - Tags (e.g., `# my_plugin`)
40
+ - Field types (String, Number, RepeatedRecord, etc.)
41
+ - Custom logic
42
+
43
+ ### 3. Render Modes
44
+
45
+ - **solidjs**: Reactive components (recommended)
46
+ - **dom**: Direct DOM manipulation
47
+
48
+ ### 4. Sizing Strategies
49
+
50
+ - **fixed**: Plugin controls its size
51
+ - **fill**: Adapts to container
52
+
53
+ ## Common Patterns
54
+
55
+ ### Chart Plugin
56
+
57
+ ```typescript
58
+ matches: (field, fieldTag, fieldType) => {
59
+ return fieldType === FieldType.RepeatedRecord && fieldTag.has('my_chart');
60
+ };
61
+ ```
62
+
63
+ ### Formatter Plugin
64
+
65
+ ```typescript
66
+ matches: (field, fieldTag, fieldType) => {
67
+ return fieldType === FieldType.Number && fieldTag.has('currency');
68
+ };
69
+ ```
70
+
71
+ ### Conditional Styling
72
+
73
+ ```typescript
74
+ renderComponent: (props) => {
75
+ const value = props.dataColumn.value;
76
+ const className = value > 100 ? 'high' : 'low';
77
+ return <div class={className}>{value}</div>;
78
+ }
79
+ ```
80
+
81
+ ## Usage in Malloy
82
+
83
+ ```malloy
84
+ source: data is table('data.csv') {
85
+ group_by:
86
+ # currency
87
+ price
88
+ # my_chart
89
+ sales_trend
90
+ }
91
+ ```
92
+
93
+ ## Registration
94
+
95
+ ```typescript
96
+ const renderer = new MalloyRenderer({
97
+ plugins: [MinimalPluginFactory],
98
+ });
99
+ ```
100
+
101
+ ## Next steps
102
+
103
+ Once your plugin reads tags beyond just the one that triggers `matches()`, read [validation.md](validation.md). It covers when to throw (red error tile), when to log (source-located validation error), and how to declare tag ownership so valid tag uses don't trigger unread-tag warnings.