1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16import {Brand} from '../../../../base/brand'; 17import {Time} from '../../../../base/time'; 18import {exists} from '../../../../base/utils'; 19import {raf} from '../../../../core/raf_scheduler'; 20import {Engine} from '../../../../trace_processor/engine'; 21import {Row} from '../../../../trace_processor/query_result'; 22import { 23 SqlValue, 24 sqlValueToReadableString, 25} from '../../../../trace_processor/sql_utils'; 26import {Arg, getArgs} from '../../../sql_utils/args'; 27import {asArgSetId} from '../../../sql_utils/core_types'; 28import {Anchor} from '../../../../widgets/anchor'; 29import {renderError} from '../../../../widgets/error'; 30import {SqlRef} from '../../../../widgets/sql_ref'; 31import {Tree, TreeNode} from '../../../../widgets/tree'; 32import {hasArgs, renderArguments} from '../../../details/slice_args'; 33import {DurationWidget} from '../../../widgets/duration'; 34import {Timestamp as TimestampWidget} from '../../../widgets/timestamp'; 35import {sqlIdRegistry} from './sql_ref_renderer_registry'; 36import {Trace} from '../../../../public/trace'; 37 38// This file contains the helper to render the details tree (based on Tree 39// widget) for an object represented by a SQL row in some table. The user passes 40// a typed schema of the tree and this impl handles fetching and rendering. 41// 42// The following types are supported: 43// Containers: 44// - dictionary (keys should be strings) 45// - array 46// Primitive values: 47// - number, string, timestamp, duration, interval and thread interval. 48// - id into another sql table. 49// - arg set id. 50// 51// For each primitive value, the user should specify a SQL expression (usually 52// just the column name). Each primitive value can be auto-skipped if the 53// underlying SQL value is null (skipIfNull). Each container can be auto-skipped 54// if empty (skipIfEmpty). 55// 56// Example of a schema: 57// { 58// 'Navigation ID': 'navigation_id', 59// 'beforeunload': SqlIdRef({ 60// source: 'beforeunload_slice_id', 61// table: 'chrome_frame_tree_nodes.id', 62// }), 63// 'initiator_origin': String({ 64// source: 'initiator_origin', 65// skipIfNull: true, 66// }), 67// 'committed_render_frame_host': { 68// 'Process ID' : 'committed_render_frame_host_process_id', 69// 'RFH ID': 'committed_render_frame_host_rfh_id', 70// }, 71// 'initial_render_frame_host': Dict({ 72// data: { 73// 'Process ID': 'committed_render_frame_host_process_id', 74// 'RFH ID': 'committed_render_frame_host_rfh_id', 75// }, 76// preview: 'printf("id=%d:%d")', committed_render_frame_host_process_id, 77// committed_render_frame_host_rfh_id)', skipIfEmpty: true, 78// }) 79// } 80 81// === Public API surface === 82 83export namespace DetailsSchema { 84 // Create a dictionary object for the schema. 85 export function Dict( 86 args: {data: {[key: string]: ValueDesc}} & ContainerParams, 87 ): DictSchema { 88 return new DictSchema(args.data, { 89 skipIfEmpty: args.skipIfEmpty, 90 }); 91 } 92 93 // Create an array object for the schema. 94 export function Arr( 95 args: {data: ValueDesc[]} & ContainerParams, 96 ): ArraySchema { 97 return new ArraySchema(args.data, { 98 skipIfEmpty: args.skipIfEmpty, 99 }); 100 } 101 102 // Create an object representing a timestamp for the schema. 103 // |ts| — SQL expression (e.g. column name) for the timestamp. 104 export function Timestamp( 105 ts: string, 106 args?: ScalarValueParams, 107 ): ScalarValueSchema { 108 return new ScalarValueSchema('timestamp', ts, args); 109 } 110 111 // Create an object representing a duration for the schema. 112 // |dur| — SQL expression (e.g. column name) for the duration. 113 export function Duration( 114 dur: string, 115 args?: ScalarValueParams, 116 ): ScalarValueSchema { 117 return new ScalarValueSchema('duration', dur, args); 118 } 119 120 // Create an object representing a time interval (timestamp + duration) 121 // for the schema. 122 // |ts|, |dur| - SQL expressions (e.g. column names) for the timestamp 123 // and duration. 124 export function Interval( 125 ts: string, 126 dur: string, 127 args?: ScalarValueParams, 128 ): IntervalSchema { 129 return new IntervalSchema(ts, dur, args); 130 } 131 132 // Create an object representing a combination of time interval and thread for 133 // the schema. 134 // |ts|, |dur|, |utid| - SQL expressions (e.g. column names) for the 135 // timestamp, duration and unique thread id. 136 export function ThreadInterval( 137 ts: string, 138 dur: string, 139 utid: string, 140 args?: ScalarValueParams, 141 ): ThreadIntervalSchema { 142 return new ThreadIntervalSchema(ts, dur, utid, args); 143 } 144 145 // Create an object representing a reference to an arg set for the schema. 146 // |argSetId| - SQL expression (e.g. column name) for the arg set id. 147 export function ArgSetId( 148 argSetId: string, 149 args?: ScalarValueParams, 150 ): ScalarValueSchema { 151 return new ScalarValueSchema('arg_set_id', argSetId, args); 152 } 153 154 // Create an object representing a SQL value for the schema. 155 // |value| - SQL expression (e.g. column name) for the value. 156 export function Value( 157 value: string, 158 args?: ScalarValueParams, 159 ): ScalarValueSchema { 160 return new ScalarValueSchema('value', value, args); 161 } 162 163 // Create an object representing string-rendered-as-url for the schema. 164 // |value| - SQL expression (e.g. column name) for the value. 165 export function URLValue( 166 value: string, 167 args?: ScalarValueParams, 168 ): ScalarValueSchema { 169 return new ScalarValueSchema('url', value, args); 170 } 171 172 export function Boolean( 173 value: string, 174 args?: ScalarValueParams, 175 ): ScalarValueSchema { 176 return new ScalarValueSchema('boolean', value, args); 177 } 178 179 // Create an object representing a reference to a SQL table row in the schema. 180 // |table| - name of the table. 181 // |id| - SQL expression (e.g. column name) for the id. 182 export function SqlIdRef( 183 table: string, 184 id: string, 185 args?: ScalarValueParams, 186 ): SqlIdRefSchema { 187 return new SqlIdRefSchema(table, id, args); 188 } 189} // namespace DetailsSchema 190 191// Params which apply to scalar values (i.e. all non-dicts and non-arrays). 192type ScalarValueParams = { 193 skipIfNull?: boolean; 194}; 195 196// Params which apply to containers (dicts and arrays). 197type ContainerParams = { 198 skipIfEmpty?: boolean; 199}; 200 201// Definition of a node in the schema. 202export type ValueDesc = 203 | DictSchema 204 | ArraySchema 205 | ScalarValueSchema 206 | IntervalSchema 207 | ThreadIntervalSchema 208 | SqlIdRefSchema 209 | string 210 | ValueDesc[] 211 | {[key: string]: ValueDesc}; 212 213// Class responsible for fetching the data and rendering the data. 214export class Details { 215 constructor( 216 private trace: Trace, 217 private sqlTable: string, 218 private id: number, 219 schema: {[key: string]: ValueDesc}, 220 ) { 221 this.dataController = new DataController( 222 trace, 223 sqlTable, 224 id, 225 sqlIdRegistry, 226 ); 227 228 this.resolvedSchema = { 229 kind: 'dict', 230 data: Object.fromEntries( 231 Object.entries(schema).map(([key, value]) => [ 232 key, 233 resolve(value, this.dataController), 234 ]), 235 ), 236 }; 237 this.dataController.fetch(); 238 } 239 240 isLoading() { 241 return this.dataController.data === undefined; 242 } 243 244 render(): m.Children { 245 if (this.dataController.data === undefined) { 246 return m('h2', 'Loading'); 247 } 248 const nodes = []; 249 for (const [key, value] of Object.entries(this.resolvedSchema.data)) { 250 nodes.push( 251 renderValue( 252 this.trace, 253 key, 254 value, 255 this.dataController.data, 256 this.dataController.sqlIdRefRenderers, 257 ), 258 ); 259 } 260 nodes.push( 261 m(TreeNode, { 262 left: 'SQL ID', 263 right: m(SqlRef, { 264 table: this.sqlTable, 265 id: this.id, 266 }), 267 }), 268 ); 269 return m(Tree, nodes); 270 } 271 272 private dataController: DataController; 273 private resolvedSchema: ResolvedDict; 274} 275 276// Type corresponding to a value which can be rendered as a part of the tree: 277// basically, it's TreeNode component without its left part. 278export type RenderedValue = { 279 // The value that should be rendered as the right part of the corresponding 280 // TreeNode. 281 value: m.Children; 282 // Values that should be rendered as the children of the corresponding 283 // TreeNode. 284 children?: m.Children; 285}; 286 287// Type describing how render an id into a given table, split into 288// async `fetch` step for fetching data and sync `render` step for generating 289// the vdom. 290export type SqlIdRefRenderer = { 291 fetch: (engine: Engine, id: bigint) => Promise<{} | undefined>; 292 render: (data: {}) => RenderedValue; 293}; 294 295// === Impl details === 296 297// Resolved index into the list of columns / expression to fetch. 298type ExpressionIndex = Brand<number, 'expression_index'>; 299// Arg sets and SQL references require a separate query to fetch the data and 300// therefore are tracked separately. 301type ArgSetIndex = Brand<number, 'arg_set_id_index'>; 302type SqlIdRefIndex = Brand<number, 'sql_id_ref'>; 303 304// Description is passed by the user and then the data is resolved into 305// "resolved" versions of the types. Description focuses on the end-user 306// ergonomics, while "Resolved" optimises for internal processing. 307 308// Description of a dict in the schema. 309class DictSchema { 310 constructor( 311 public data: {[key: string]: ValueDesc}, 312 public params?: ContainerParams, 313 ) {} 314} 315 316// Resolved version of a dict. 317type ResolvedDict = { 318 kind: 'dict'; 319 data: {[key: string]: ResolvedValue}; 320} & ContainerParams; 321 322// Description of an array in the schema. 323class ArraySchema { 324 constructor( 325 public data: ValueDesc[], 326 public params?: ContainerParams, 327 ) {} 328} 329 330// Resolved version of an array. 331type ResolvedArray = { 332 kind: 'array'; 333 data: ResolvedValue[]; 334} & ContainerParams; 335 336// Schema for all simple scalar values (ones that need to fetch only one value 337// from SQL). 338class ScalarValueSchema { 339 constructor( 340 public kind: 341 | 'timestamp' 342 | 'duration' 343 | 'arg_set_id' 344 | 'value' 345 | 'url' 346 | 'boolean', 347 public sourceExpression: string, 348 public params?: ScalarValueParams, 349 ) {} 350} 351 352// Resolved version of simple scalar values. 353type ResolvedScalarValue = { 354 kind: 'timestamp' | 'duration' | 'value' | 'url' | 'boolean'; 355 source: ExpressionIndex; 356} & ScalarValueParams; 357 358// Resolved version of arg set. 359type ResolvedArgSet = { 360 kind: 'arg_set_id'; 361 source: ArgSetIndex; 362} & ScalarValueParams; 363 364// Schema for a time interval (ts, dur pair). 365class IntervalSchema { 366 constructor( 367 public ts: string, 368 public dur: string, 369 public params?: ScalarValueParams, 370 ) {} 371} 372 373// Resolved version of a time interval. 374type ResolvedInterval = { 375 kind: 'interval'; 376 ts: ExpressionIndex; 377 dur: ExpressionIndex; 378} & ScalarValueParams; 379 380// Schema for a time interval for a given thread (ts, dur, utid triple). 381class ThreadIntervalSchema { 382 constructor( 383 public ts: string, 384 public dur: string, 385 public utid: string, 386 public params?: ScalarValueParams, 387 ) {} 388} 389 390// Resolved version of a time interval for a given thread. 391type ResolvedThreadInterval = { 392 kind: 'thread_interval'; 393 ts: ExpressionIndex; 394 dur: ExpressionIndex; 395 utid: ExpressionIndex; 396} & ScalarValueParams; 397 398// Schema for a reference to a SQL table row. 399class SqlIdRefSchema { 400 constructor( 401 public table: string, 402 public id: string, 403 public params?: ScalarValueParams, 404 ) {} 405} 406 407type ResolvedSqlIdRef = { 408 kind: 'sql_id_ref'; 409 ref: SqlIdRefIndex; 410} & ScalarValueParams; 411 412type ResolvedValue = 413 | ResolvedDict 414 | ResolvedArray 415 | ResolvedScalarValue 416 | ResolvedArgSet 417 | ResolvedInterval 418 | ResolvedThreadInterval 419 | ResolvedSqlIdRef; 420 421// Helper class to store the error messages while fetching the data. 422class Err { 423 constructor(public message: string) {} 424} 425 426// Fetched data from SQL which is needed to render object according to the given 427// schema. 428interface Data { 429 // Source of the expressions that were fetched. 430 valueExpressions: string[]; 431 // Fetched values. 432 values: SqlValue[]; 433 434 // Source statements for the arg sets. 435 argSetExpressions: string[]; 436 // Fetched arg sets. 437 argSets: (Arg[] | Err)[]; 438 439 // Source statements for the SQL references. 440 sqlIdRefs: {tableName: string; idExpression: string}[]; 441 // Fetched data for the SQL references. 442 sqlIdRefData: ( 443 | { 444 data: {}; 445 id: bigint | null; 446 } 447 | Err 448 )[]; 449} 450 451// Class responsible for collecting the description of the data to fetch and 452// fetching it. 453class DataController { 454 // List of expressions to fetch. Resolved values will have indexes into this 455 // list. 456 expressions: string[] = []; 457 // List of arg sets to fetch. Arg set ids are fetched first (together with 458 // other scalar values as a part of the `expressions` list) and then the arg 459 // sets themselves are fetched. 460 argSets: ExpressionIndex[] = []; 461 // List of SQL references to fetch. SQL reference ids are fetched first 462 // (together with other scalar values as a part of the `expressions` list) and 463 // then the SQL references themselves are fetched. 464 sqlIdRefs: {id: ExpressionIndex; tableName: string}[] = []; 465 466 // Fetched data. 467 data?: Data; 468 469 constructor( 470 private trace: Trace, 471 private sqlTable: string, 472 private id: number, 473 public sqlIdRefRenderers: {[table: string]: SqlIdRefRenderer}, 474 ) {} 475 476 // Fetch the data. `expressions` and other lists must be populated first by 477 // resolving the schema. 478 async fetch() { 479 const data: Data = { 480 valueExpressions: this.expressions, 481 values: [], 482 argSetExpressions: this.argSets.map((index) => this.expressions[index]), 483 argSets: [], 484 sqlIdRefs: this.sqlIdRefs.map((ref) => ({ 485 tableName: ref.tableName, 486 idExpression: this.expressions[ref.id], 487 })), 488 sqlIdRefData: [], 489 }; 490 491 // Helper to generate the labels for the expressions. 492 const label = (index: number) => `col_${index}`; 493 494 // Fetch the scalar values for the basic expressions. 495 const row: Row = ( 496 await this.trace.engine.query(` 497 SELECT 498 ${this.expressions 499 .map((value, index) => `${value} as ${label(index)}`) 500 .join(',\n')} 501 FROM ${this.sqlTable} 502 WHERE id = ${this.id} 503 `) 504 ).firstRow({}); 505 for (let i = 0; i < this.expressions.length; ++i) { 506 data.values.push(row[label(i)]); 507 } 508 509 // Fetch the arg sets based on the fetched arg set ids. 510 for (const argSetIndex of this.argSets) { 511 const argSetId = data.values[argSetIndex]; 512 if (argSetId === null) { 513 data.argSets.push([]); 514 } else if (typeof argSetId !== 'number' && typeof argSetId !== 'bigint') { 515 data.argSets.push( 516 new Err( 517 `Incorrect type for arg set ${ 518 data.argSetExpressions[argSetIndex] 519 }: expected a number, got ${typeof argSetId} instead}`, 520 ), 521 ); 522 } else { 523 data.argSets.push( 524 await getArgs(this.trace.engine, asArgSetId(Number(argSetId))), 525 ); 526 } 527 } 528 529 // Fetch the data for SQL references based on fetched ids. 530 for (const ref of this.sqlIdRefs) { 531 const renderer = this.sqlIdRefRenderers[ref.tableName]; 532 if (renderer === undefined) { 533 data.sqlIdRefData.push(new Err(`Unknown table ${ref.tableName}`)); 534 continue; 535 } 536 const id = data.values[ref.id]; 537 if (id === null) { 538 data.sqlIdRefData.push({data: {}, id}); 539 continue; 540 } else if (typeof id !== 'bigint') { 541 data.sqlIdRefData.push( 542 new Err( 543 `Incorrect type for SQL reference ${ 544 data.valueExpressions[ref.id] 545 }: expected a bigint, got ${typeof id} instead}`, 546 ), 547 ); 548 continue; 549 } 550 const refData = await renderer.fetch(this.trace.engine, id); 551 if (refData === undefined) { 552 data.sqlIdRefData.push( 553 new Err( 554 `Failed to fetch the data with id ${id} for table ${ref.tableName}`, 555 ), 556 ); 557 continue; 558 } 559 data.sqlIdRefData.push({data: refData, id}); 560 } 561 562 this.data = data; 563 raf.scheduleFullRedraw(); 564 } 565 566 // Add a given expression to the list of expressions to fetch and return its 567 // index. 568 addExpression(expr: string): ExpressionIndex { 569 const result = this.expressions.length; 570 this.expressions.push(expr); 571 return result as ExpressionIndex; 572 } 573 574 // Add a given arg set to the list of arg sets to fetch and return its index. 575 addArgSet(expr: string): ArgSetIndex { 576 const result = this.argSets.length; 577 this.argSets.push(this.addExpression(expr)); 578 return result as ArgSetIndex; 579 } 580 581 // Add a given SQL reference to the list of SQL references to fetch and return 582 // its index. 583 addSqlIdRef(tableName: string, idExpr: string): SqlIdRefIndex { 584 const result = this.sqlIdRefs.length; 585 this.sqlIdRefs.push({ 586 tableName, 587 id: this.addExpression(idExpr), 588 }); 589 return result as SqlIdRefIndex; 590 } 591} 592 593// Resolve a given schema into a resolved version, normalising the schema and 594// computing the list of data to fetch. 595function resolve(schema: ValueDesc, data: DataController): ResolvedValue { 596 if (typeof schema === 'string') { 597 return { 598 kind: 'value', 599 source: data.addExpression(schema), 600 }; 601 } 602 if (Array.isArray(schema)) { 603 return { 604 kind: 'array', 605 data: schema.map((x) => resolve(x, data)), 606 }; 607 } 608 if (schema instanceof ArraySchema) { 609 return { 610 kind: 'array', 611 data: schema.data.map((x) => resolve(x, data)), 612 ...schema.params, 613 }; 614 } 615 if (schema instanceof ScalarValueSchema) { 616 if (schema.kind === 'arg_set_id') { 617 return { 618 kind: schema.kind, 619 source: data.addArgSet(schema.sourceExpression), 620 ...schema.params, 621 }; 622 } else { 623 return { 624 kind: schema.kind, 625 source: data.addExpression(schema.sourceExpression), 626 ...schema.params, 627 }; 628 } 629 } 630 if (schema instanceof IntervalSchema) { 631 return { 632 kind: 'interval', 633 ts: data.addExpression(schema.ts), 634 dur: data.addExpression(schema.dur), 635 ...schema.params, 636 }; 637 } 638 if (schema instanceof ThreadIntervalSchema) { 639 return { 640 kind: 'thread_interval', 641 ts: data.addExpression(schema.ts), 642 dur: data.addExpression(schema.dur), 643 utid: data.addExpression(schema.utid), 644 ...schema.params, 645 }; 646 } 647 if (schema instanceof SqlIdRefSchema) { 648 return { 649 kind: 'sql_id_ref', 650 ref: data.addSqlIdRef(schema.table, schema.id), 651 ...schema.params, 652 }; 653 } 654 if (schema instanceof DictSchema) { 655 return { 656 kind: 'dict', 657 data: Object.fromEntries( 658 Object.entries(schema.data).map(([key, value]) => [ 659 key, 660 resolve(value, data), 661 ]), 662 ), 663 ...schema.params, 664 }; 665 } 666 return { 667 kind: 'dict', 668 data: Object.fromEntries( 669 Object.entries(schema).map(([key, value]) => [key, resolve(value, data)]), 670 ), 671 }; 672} 673 674// Generate the vdom for a given value using the fetched `data`. 675function renderValue( 676 trace: Trace, 677 key: string, 678 value: ResolvedValue, 679 data: Data, 680 sqlIdRefRenderers: {[table: string]: SqlIdRefRenderer}, 681): m.Children { 682 switch (value.kind) { 683 case 'value': 684 if (data.values[value.source] === null && value.skipIfNull) return null; 685 return m(TreeNode, { 686 left: key, 687 right: sqlValueToReadableString(data.values[value.source]), 688 }); 689 case 'url': { 690 const url = data.values[value.source]; 691 let rhs: m.Children; 692 if (url === null) { 693 if (value.skipIfNull) return null; 694 rhs = renderNull(); 695 } else if (typeof url !== 'string') { 696 rhs = renderError( 697 `Incorrect type for URL ${ 698 data.valueExpressions[value.source] 699 }: expected string, got ${typeof url}`, 700 ); 701 } else { 702 rhs = m( 703 Anchor, 704 {href: url, target: '_blank', icon: 'open_in_new'}, 705 url, 706 ); 707 } 708 return m(TreeNode, { 709 left: key, 710 right: rhs, 711 }); 712 } 713 case 'boolean': { 714 const bool = data.values[value.source]; 715 if (bool === null && value.skipIfNull) return null; 716 let rhs: m.Child; 717 if (typeof bool !== 'bigint' && typeof bool !== 'number') { 718 rhs = renderError( 719 `Incorrect type for boolean ${ 720 data.valueExpressions[value.source] 721 }: expected bigint or number, got ${typeof bool}`, 722 ); 723 } else { 724 rhs = bool ? 'true' : 'false'; 725 } 726 return m(TreeNode, {left: key, right: rhs}); 727 } 728 case 'timestamp': { 729 const ts = data.values[value.source]; 730 let rhs: m.Child; 731 if (ts === null) { 732 if (value.skipIfNull) return null; 733 rhs = m('i', 'NULL'); 734 } else if (typeof ts !== 'bigint') { 735 rhs = renderError( 736 `Incorrect type for timestamp ${ 737 data.valueExpressions[value.source] 738 }: expected bigint, got ${typeof ts}`, 739 ); 740 } else { 741 rhs = m(TimestampWidget, { 742 ts: Time.fromRaw(ts), 743 }); 744 } 745 return m(TreeNode, { 746 left: key, 747 right: rhs, 748 }); 749 } 750 case 'duration': { 751 const dur = data.values[value.source]; 752 return m(TreeNode, { 753 left: key, 754 right: 755 typeof dur === 'bigint' && 756 m(DurationWidget, { 757 dur, 758 }), 759 }); 760 } 761 case 'interval': 762 case 'thread_interval': { 763 const dur = data.values[value.dur]; 764 return m(TreeNode, { 765 left: key, 766 right: 767 typeof dur === 'bigint' && 768 m(DurationWidget, { 769 dur, 770 }), 771 }); 772 } 773 case 'sql_id_ref': 774 const ref = data.sqlIdRefs[value.ref]; 775 const refData = data.sqlIdRefData[value.ref]; 776 let rhs: m.Children; 777 let children: m.Children; 778 if (refData instanceof Err) { 779 rhs = renderError(refData.message); 780 } else if (refData.id === null && value.skipIfNull === true) { 781 rhs = renderNull(); 782 } else { 783 const renderer = sqlIdRefRenderers[ref.tableName]; 784 if (renderer === undefined) { 785 rhs = renderError( 786 `Unknown table ${ref.tableName} (${ref.tableName}[${refData.id}])`, 787 ); 788 } else { 789 const rendered = renderer.render(refData.data); 790 rhs = rendered.value; 791 children = rendered.children; 792 } 793 } 794 return m( 795 TreeNode, 796 { 797 left: key, 798 right: rhs, 799 }, 800 children, 801 ); 802 case 'arg_set_id': 803 const args = data.argSets[value.source]; 804 if (args instanceof Err) { 805 return renderError(args.message); 806 } 807 return ( 808 hasArgs(args) && 809 m( 810 TreeNode, 811 { 812 left: key, 813 }, 814 renderArguments(trace, args), 815 ) 816 ); 817 case 'array': { 818 const children: m.Children[] = []; 819 for (const child of value.data) { 820 const renderedChild = renderValue( 821 trace, 822 `[${children.length}]`, 823 child, 824 data, 825 sqlIdRefRenderers, 826 ); 827 if (exists(renderedChild)) { 828 children.push(renderedChild); 829 } 830 } 831 if (children.length === 0 && value.skipIfEmpty) { 832 return null; 833 } 834 return m( 835 TreeNode, 836 { 837 left: key, 838 }, 839 children, 840 ); 841 } 842 case 'dict': { 843 const children: m.Children[] = []; 844 for (const [key, val] of Object.entries(value.data)) { 845 const child = renderValue(trace, key, val, data, sqlIdRefRenderers); 846 if (exists(child)) { 847 children.push(child); 848 } 849 } 850 if (children.length === 0 && value.skipIfEmpty) { 851 return null; 852 } 853 return m( 854 TreeNode, 855 { 856 left: key, 857 }, 858 children, 859 ); 860 } 861 } 862} 863 864function renderNull(): m.Children { 865 return m('i', 'NULL'); 866} 867