Skip to content

Commit a5a4b62

Browse files
authored
TSL: Introduce StackTrace (#32914)
1 parent 76ff395 commit a5a4b62

File tree

24 files changed

+326
-38
lines changed

24 files changed

+326
-38
lines changed

examples/jsm/inspector/Inspector.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class Inspector extends RendererInspector {
110110

111111
}
112112

113-
resolveConsole( type, message ) {
113+
resolveConsole( type, message, stackTrace = null ) {
114114

115115
switch ( type ) {
116116

@@ -126,15 +126,31 @@ class Inspector extends RendererInspector {
126126

127127
this.console.addMessage( 'warn', message );
128128

129-
console.warn( message );
129+
if ( stackTrace && stackTrace.isStackTrace ) {
130+
131+
console.warn( stackTrace.getError( message ) );
132+
133+
} else {
134+
135+
console.warn( message );
136+
137+
}
130138

131139
break;
132140

133141
case 'error':
134142

135143
this.console.addMessage( 'error', message );
136144

137-
console.error( message );
145+
if ( stackTrace && stackTrace.isStackTrace ) {
146+
147+
console.error( stackTrace.getError( message ) );
148+
149+
} else {
150+
151+
console.error( message );
152+
153+
}
138154

139155
break;
140156

src/nodes/Nodes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export { default as NodeAttribute } from './core/NodeAttribute.js';
1919
export { default as NodeBuilder } from './core/NodeBuilder.js';
2020
export { default as NodeCache } from './core/NodeCache.js';
2121
export { default as NodeCode } from './core/NodeCode.js';
22+
export { default as NodeError } from './core/NodeError.js';
2223
export { default as NodeFrame } from './core/NodeFrame.js';
2324
export { default as NodeFunctionInput } from './core/NodeFunctionInput.js';
2425
export { default as NodeUniform } from './core/NodeUniform.js';
@@ -28,6 +29,7 @@ export { default as OutputStructNode } from './core/OutputStructNode.js';
2829
export { default as ParameterNode } from './core/ParameterNode.js';
2930
export { default as PropertyNode } from './core/PropertyNode.js';
3031
export { default as StackNode } from './core/StackNode.js';
32+
export { default as StackTrace } from './core/StackTrace.js';
3133
export { default as StructNode } from './core/StructNode.js';
3234
export { default as StructTypeNode } from './core/StructTypeNode.js';
3335
export { default as SubBuildNode } from './core/SubBuildNode.js';

src/nodes/accessors/TextureNode.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { Compatibility, IntType, LessCompare, NearestFilter, UnsignedIntType } f
1313
import { Texture } from '../../textures/Texture.js';
1414
import { warn, warnOnce } from '../../utils.js';
1515

16+
import NodeError from '../core/NodeError.js';
17+
1618
const EmptyTexture = /*@__PURE__*/ new Texture();
1719

1820
/**
@@ -344,7 +346,7 @@ class TextureNode extends UniformNode {
344346

345347
if ( ! texture || texture.isTexture !== true ) {
346348

347-
throw new Error( 'THREE.TSL: `texture( value )` function expects a valid instance of THREE.Texture().' );
349+
throw new NodeError( 'THREE.TSL: `texture( value )` function expects a valid instance of THREE.Texture().', this.stackTrace );
348350

349351
}
350352

src/nodes/core/Node.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { EventDispatcher } from '../../core/EventDispatcher.js';
55
import { MathUtils } from '../../math/MathUtils.js';
66
import { warn, error } from '../../utils.js';
77

8+
import StackTrace from './StackTrace.js';
9+
810
const _parentBuildStage = {
911
analyze: 'setup',
1012
generate: 'analyze'
@@ -142,6 +144,20 @@ class Node extends EventDispatcher {
142144

143145
Object.defineProperty( this, 'id', { value: _nodeId ++ } );
144146

147+
/**
148+
* The stack trace of the node for debugging purposes.
149+
*
150+
* @type {?string}
151+
* @default null
152+
*/
153+
this.stackTrace = null;
154+
155+
if ( Node.captureStackTrace === true ) {
156+
157+
this.stackTrace = new StackTrace();
158+
159+
}
160+
145161
}
146162

147163
/**
@@ -1080,4 +1096,12 @@ class Node extends EventDispatcher {
10801096

10811097
}
10821098

1099+
/**
1100+
* Enables or disables the automatic capturing of stack traces for nodes.
1101+
*
1102+
* @type {boolean}
1103+
* @default false
1104+
*/
1105+
Node.captureStackTrace = false;
1106+
10831107
export default Node;

src/nodes/core/NodeError.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Custom error class for node-related errors, including stack trace information.
3+
*/
4+
class NodeError extends Error {
5+
6+
constructor( message, stackTrace = null ) {
7+
8+
super( message );
9+
10+
/**
11+
* The name of the error.
12+
*
13+
* @type {string}
14+
*/
15+
this.name = 'NodeError';
16+
17+
/**
18+
* The stack trace associated with the error.
19+
*
20+
* @type {?StackTrace}
21+
*/
22+
this.stackTrace = stackTrace;
23+
24+
}
25+
26+
}
27+
28+
export default NodeError;

src/nodes/core/NodeUtils.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { Matrix4 } from '../../math/Matrix4.js';
55
import { Vector2 } from '../../math/Vector2.js';
66
import { Vector3 } from '../../math/Vector3.js';
77
import { Vector4 } from '../../math/Vector4.js';
8+
89
import { error } from '../../utils.js';
10+
import StackTrace from '../core/StackTrace.js';
911

1012
// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
1113
// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
@@ -154,7 +156,7 @@ export function getLengthFromType( type ) {
154156
if ( /mat3/.test( type ) ) return 9;
155157
if ( /mat4/.test( type ) ) return 16;
156158

157-
error( 'TSL: Unsupported type:', type );
159+
error( `TSL: Unsupported type: ${ type }`, new StackTrace() );
158160

159161
}
160162

@@ -176,7 +178,7 @@ export function getMemoryLengthFromType( type ) {
176178
if ( /mat3/.test( type ) ) return 12;
177179
if ( /mat4/.test( type ) ) return 16;
178180

179-
error( 'TSL: Unsupported type:', type );
181+
error( `TSL: Unsupported type: ${ type }`, new StackTrace() );
180182

181183
}
182184

@@ -198,7 +200,7 @@ export function getAlignmentFromType( type ) {
198200
if ( /mat3/.test( type ) ) return 16;
199201
if ( /mat4/.test( type ) ) return 16;
200202

201-
error( 'TSL: Unsupported type:', type );
203+
error( `TSL: Unsupported type: ${ type }`, new StackTrace() );
202204

203205
}
204206

src/nodes/core/ParameterNode.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { error } from '../../utils.js';
2+
import StackTrace from '../core/StackTrace.js';
23
import PropertyNode from './PropertyNode.js';
34

45
/**
@@ -55,7 +56,7 @@ class ParameterNode extends PropertyNode {
5556

5657
} else {
5758

58-
error( `TSL: Member "${ name }" not found in struct "${ type }".` );
59+
error( `TSL: Member "${ name }" not found in struct "${ type }".`, new StackTrace() );
5960

6061
memberType = 'float';
6162

src/nodes/core/StackNode.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Node from './Node.js';
2+
import StackTrace from '../core/StackTrace.js';
23
import { select } from '../math/ConditionalNode.js';
34
import { ShaderNode, nodeProxy, getCurrentStack, setCurrentStack, nodeObject } from '../tsl/TSLBase.js';
45
import { error } from '../../utils.js';
@@ -117,7 +118,7 @@ class StackNode extends Node {
117118

118119
if ( node.isNode !== true ) {
119120

120-
error( 'TSL: Invalid node added to stack.' );
121+
error( 'TSL: Invalid node added to stack.', new StackTrace() );
121122
return this;
122123

123124
}
@@ -229,7 +230,7 @@ class StackNode extends Node {
229230

230231
} else {
231232

232-
error( 'TSL: Invalid parameter length. Case() requires at least two parameters.' );
233+
error( 'TSL: Invalid parameter length. Case() requires at least two parameters.', new StackTrace() );
233234

234235
}
235236

src/nodes/core/StackTrace.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Pre-compiled RegExp patterns for ignored files
2+
const IGNORED_FILES = [
3+
/^StackTrace\.js$/,
4+
/^TSLCore\.js$/,
5+
/^.*Node\.js$/,
6+
/^three\.webgpu.*\.js$/
7+
];
8+
9+
/**
10+
* Parses the stack trace and filters out ignored files.
11+
* Returns an array with function name, file, line, and column.
12+
*/
13+
function getFilteredStack( stack ) {
14+
15+
// Pattern to extract function name, file, line, and column from different browsers
16+
// Chrome: "at functionName (file.js:1:2)" or "at file.js:1:2"
17+
// Firefox: "[email protected]:1:2"
18+
const regex = /(?:at\s+(.+?)\s+\()?(?:(.+?)@)?([^@\s()]+):(\d+):(\d+)/;
19+
20+
return stack.split( '\n' )
21+
.map( line => {
22+
23+
const match = line.match( regex );
24+
if ( ! match ) return null; // Skip if line format is invalid
25+
26+
// Chrome: match[1], Firefox: match[2]
27+
const fn = match[ 1 ] || match[ 2 ] || '';
28+
const file = match[ 3 ].split( '?' )[ 0 ]; // Clean file name (Vite/HMR)
29+
const lineNum = parseInt( match[ 4 ], 10 );
30+
const column = parseInt( match[ 5 ], 10 );
31+
32+
// Extract only the filename from full path
33+
const fileName = file.split( '/' ).pop();
34+
35+
return {
36+
fn: fn,
37+
file: fileName,
38+
line: lineNum,
39+
column: column
40+
};
41+
42+
} )
43+
.filter( frame => {
44+
45+
// Only keep frames that are valid and not in the ignore list
46+
return frame && ! IGNORED_FILES.some( regex => regex.test( frame.file ) );
47+
48+
} );
49+
50+
}
51+
52+
/**
53+
* Class representing a stack trace for debugging purposes.
54+
*/
55+
class StackTrace {
56+
57+
/**
58+
* Creates a StackTrace instance by capturing and filtering the current stack trace.
59+
*
60+
* @param {Error|string|null} stackMessage - An optional stack trace to use instead of capturing a new one.
61+
*/
62+
constructor( stackMessage = null ) {
63+
64+
/**
65+
* This flag can be used for type testing.
66+
*
67+
* @type {boolean}
68+
* @readonly
69+
* @default true
70+
*/
71+
this.isStackTrace = true;
72+
73+
/**
74+
* The stack trace.
75+
*
76+
* @type {Array<{fn: string, file: string, line: number, column: number}>}
77+
*/
78+
this.stack = getFilteredStack( stackMessage ? stackMessage : new Error().stack );
79+
80+
}
81+
82+
/**
83+
* Returns a formatted location string of the top stack frame.
84+
*
85+
* @returns {string} The formatted stack trace message.
86+
*/
87+
getLocation() {
88+
89+
if ( this.stack.length === 0 ) {
90+
91+
return '[Unknown location]';
92+
93+
}
94+
95+
const mainStack = this.stack[ 0 ];
96+
97+
const fn = mainStack.fn;
98+
const fnName = fn ? `"${ fn }()" at ` : '';
99+
100+
return `${fnName}"${mainStack.file}:${mainStack.line}"`; // :${mainStack.column}
101+
102+
}
103+
104+
/**
105+
* Returns the full error message including the stack trace.
106+
*
107+
* @param {string} message - The error message.
108+
* @returns {string} The full error message with stack trace.
109+
*/
110+
getError( message ) {
111+
112+
if ( this.stack.length === 0 ) {
113+
114+
return message;
115+
116+
}
117+
118+
// Output: "Error: message\n at functionName (file.js:line:column)"
119+
const stackString = this.stack.map( frame => {
120+
121+
const location = `${ frame.file }:${ frame.line }:${ frame.column }`;
122+
123+
if ( frame.fn ) {
124+
125+
return ` at ${ frame.fn } (${ location })`;
126+
127+
}
128+
129+
return ` at ${ location }`;
130+
131+
} ).join( '\n' );
132+
133+
return `${ message }\n${ stackString }`;
134+
135+
}
136+
137+
}
138+
139+
export default StackTrace;

src/nodes/core/UniformNode.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import InputNode from './InputNode.js';
2+
import StackTrace from '../core/StackTrace.js';
23
import { objectGroup } from './UniformGroupNode.js';
34
import { getConstNodeType } from '../tsl/TSLCore.js';
45
import { getValueFromType } from './NodeUtils.js';
@@ -78,7 +79,7 @@ class UniformNode extends InputNode {
7879
*/
7980
label( name ) {
8081

81-
warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.' ); // @deprecated r179
82+
warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.', new StackTrace() ); // @deprecated r179
8283

8384
return this.setName( name );
8485

0 commit comments

Comments
 (0)