skip to Main Content

In trying to debug a very rare bug in tests I’m writing, I decided to add a very homebrew tracing mechanism to my code. Basically I have

#tracer?: Array<string>;
myclass.setTracer(tracer?: Array<string>) {
   this.#tracer = tracer;
}

#trace(msg: string) {
if (this.#tracer) {
   this.#tracer.push(msg);
}

I can then add a call to setTracer(someArrayOfString) and pepper my code with

this.#trace("my message");

and if I fail, print out the trace I have in someArrayOfStrings to try and figure out what went wrong.

However, this would seemingly have a performance hit, especially if I’m creating unique strings (i.e. interpolating variables).

I figure, I could probably do something like to avoid the string creation (though maybe I’m wrong about the optimizations)

(this.#tracer !== undefined) && this.#tracer.push(msg);

but I find peppering my code with that instead of the the "cleaner" this.#trace() call (that effectively does the same logical thing, but only after string has been created).

Is it possible to improve this? I assume others have thought of this, and perhaps have better solutions that I’d be willing to implement (just don’t know them).

any recommendations would be appreciated.

2

Answers


  1. Preface: Rather than doing something bespoke, you might consider looking into existing tools for doing tracing/instrumention. But taking the question as asked:

    You can avoid the string interpolation by making the #trace method, itself, the thing that’s either undefined or a function:

    class Example {
        #tracer?: Array<string>;
        #trace?: (msg: string) => void;
    
        setTracer(tracer?: Array<string>) {
            this.#tracer = tracer;
            if (tracer) {
                this.#trace = (msg) => {
                    tracer.push(msg);
                };
            } else {
                this.#trace = undefined;
            }
        }
    }
    

    Then use optional chaining on the actual calls:

    this.#trace?.(`a = ${a}, b = ${b}`);
    //         ^^
    

    If that statement executes when this.#trace is undefined, the string interpolation is never performed, because the statement short-circuits as of the optional chaining (.?). That’s specified behavior.

    In JavaScript you’d have the concern that you could easily forget the .?, but not in TypeScript; TypeScript will show you an error if you forget it, thanks to #trace potentially being undefined.

    If you want to prove that the interpolation doesn’t happen to yourself, you can do one with a side-effect:

    const sideEffect = () => {
        console.log("interpolated!");
        return 42;
    };
    this.#trace?.(`${sideEffect()}`);
    

    Example in JavaScript:

    class Example {
        #tracer;
        #trace;
    
        setTracer(tracer) {
            this.#tracer = tracer;
            if (tracer) {
                this.#trace = (msg) => {
                    tracer.push(msg);
                };
            } else {
                this.#trace = undefined;
            }
        }
    
        doSomething() {
            const sideEffect = () => {
                console.log("interpolated!");
                return 42;
            };
            this.#trace?.(`${sideEffect()}`);
        }
    }
    
    const ex = new Example();
    
    console.log("doSomething without tracing:");
    ex.doSomething();
    
    ex.setTracer([]);
    console.log("doSomething with tracing:");
    ex.doSomething();
    Login or Signup to reply.
  2. I recommend using the empty function instead of any check for #trace or #tracer.

    For example:

    #trace = () => {}; /* always keep that is empy function */
    setTracer (/*String[]*/ tracer) {
         if (tracer) {
             this.#trace = (msg) => tracer.push(msg);
         } else {
             this.#trace = () => {};
         }
    }
    

    Below is my test code, assuming a production environment where you don’t use setTracer:

    const LoopEffort = 100 * 1000 * 1000;
    
    class TestRunner1 {
        #trace = () => {}; /* always keep that is empy function */
        
        setTracer (/*String[]*/ tracer) {
            if (tracer) {
                this.#trace = (msg) => tracer.push(msg);
            } else {
                this.#trace = () => {};
            }
        }
    
        run (timeLabel) {
            console.time(timeLabel);
            for(let i = 0; i < LoopEffort; i++) {
                this.#trace("OK");
            }
            console.timeEnd(timeLabel);
        }
    }
    
    class TestRunner2 {
        #trace = undefined;
        
        setTracer (/*String[]*/ tracer) {
            if (tracer) {
                this.#trace = (msg) => tracer.push(msg);
            } else {
                this.#trace = undefined;
            }
        }
    
        run (timeLabel) {
            console.time(timeLabel);
            for(let i = 0; i < LoopEffort; i++) {
                this.#trace?.("OK");
            }
            console.timeEnd(timeLabel);
        }
    }
    
    class TestRunner3 {
        #tracer = undefined;
        
        #trace (msg) {
            if (this.#tracer) {
                this.#tracer.push(msg);
            }
        }
        
        setTracer (/*String[]*/ tracer) {
            this.#tracer = tracer;
        }
    
        run (timeLabel) {
            console.time(timeLabel);
            for(let i = 0; i < LoopEffort; i++) {
                this.#trace("OK");
            }
            console.timeEnd(timeLabel);
        }
    }
    
    class TestRunner4 {
        #tracer = undefined;
        
        #trace (msg) {
             this.#tracer?.push(msg);
        }
        
        setTracer (/*String[]*/ tracer) {
            this.#tracer = tracer;
        }
    
        run (timeLabel) {
            console.time(timeLabel);
            for(let i = 0; i < LoopEffort; i++) {
                this.#trace("OK");
            }
            console.timeEnd(timeLabel);
        }
    }
    
    /* execute */
    new TestRunner1().run("Using empty function");
    new TestRunner2().run("Using optional chaining on #trace");
    new TestRunner3().run("Check null of #tracer");
    new TestRunner4().run("Using optional chaining on #tracer");

    Using an empty function completely beats the other contenders!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search