13

I'm trying to write a function that measures the execution time of another function:

export class Profiler {
    public measureSyncFunc(fn: () => any): Promise<number> {
        return new Promise<number>((resolve, reject) => {
            let elapsed = 0;

            let intervalId = window.setInterval(() => {
                elapsed += 1; // this is never called
            }, 1);

            this.execFunc(fn)
                .then((result: any) => {
                    window.clearInterval(intervalId);
                    resolve(elapsed);
                });
        });
    }

    private execFunc(fn: () => any): Promise<any> {
        return new Promise<any>((resolve, reject) => {
            resolve(fn());
        });
    }
}

Then I use it like that:

let array = generateRandomArray(100000);

instance.measureSyncFunc(bubbleSort(array))
    .then((elapsed: number) => {
        console.log(`end session: ${elapsed} seconds`);
        resolve();
    });

The bubbleSort function is synchronous and it takes several seconds to complete. See code here:

The result in the console is "end session: 0 seconds" because the interval callback is never called.

Do you know how I can make it called ? Thank you very much guys !

Vivien Adnot
  • 1,157
  • 3
  • 14
  • 30
  • 2
    `=+` is an invalid operator – charlietfl May 24 '17 at 12:12
  • 1
    note `instance.measureSyncFunc(bubbleSort(array))` you're passing the **result of calling** bubbleSort to your `measureSyncFunc`, but `measureSyncFunc` expects a function as an argument – Jaromanda X May 24 '17 at 12:12
  • 1
    `setInterval` of 1ms - you can't actually get an interval that small - have you considered the profiling tools available in most browsers these days? `performance.now()` or `console.time('xxx') / console.timeEnd('xxx')` – Jaromanda X May 24 '17 at 12:14
  • 2
    @charlietfl `invalid operator` - actually, that would result in `elapsed == 1`, permanently :p – Jaromanda X May 24 '17 at 12:17

5 Answers5

13

If the functions you want to measure will always be synchronous there's really no need to involve promises.

Since the function you want to test takes parameters you it's best to to wrap it in an arrow function in order to be able to call it with another context and not have to manage it's parameters yourself.

Something simple like this will do just fine.

function measure(fn: () => void): number {
    let start = performance.now();
    fn();
    return performance.now() - start;
}

function longRunningFunction(n: number) {
    for (let i = 0; i < n; i++) {
        console.log(i);
    }
}

let duration = measure(() => {
    longRunningFunction(100);
});

console.log(`took ${duration} ms`);

If you want to measure the time it takes an async function (if it returns a promise) to resolve you can easily change the code to something like this:

function measurePromise(fn: () => Promise<any>): Promise<number> {
    let onPromiseDone = () => performance.now() - start;

    let start = performance.now();
    return fn().then(onPromiseDone, onPromiseDone);
}

function longPromise(delay: number) {
    return new Promise<string>((resolve) => {
        setTimeout(() => {
            resolve('Done');
        }, delay);
    });
}

measurePromise(() => longPromise(300))
    .then((duration) => {
        console.log(`promise took ${duration} ms`);
    });

Note: This solution uses the ES6 Promise, if you are using something else you might have to adapt it but the logic should be the same.

You can see both examples working in the playground here.

toskv
  • 30,680
  • 7
  • 72
  • 74
  • And as @JaromandaX pointed out in a comment to the question, perhaps the most important change is not to pass the _result_ of the long-running function into the timing function. – TripeHound May 24 '17 at 12:55
  • indeed, for sync functions at least that means the entire execution is already done. I'll add that as a remark in the answer if you don't mind. :) – toskv May 24 '17 at 12:56
  • By all means (but the credit goes to @JaromandaX). However good/bad the rest of the OP's code was, it couldn't possibly have worked without the lambda construct (or equivalent). – TripeHound May 24 '17 at 13:40
  • normal function would do too, but arrow functions are more flexible in this case.. – toskv May 24 '17 at 13:41
  • 2
    It should be noted that this approach is [not reliable](https://jsfiddle.net/mindplay/sq81op7k/) when running multiple promises in parallel - if the promises do any amount of blocking work, the results will be more or less random. I have tried all the approaches in this thread (as of april 13th '22) and none of them work. I don't think there's any reliable way to time promises. If you run x promises in parallel, what you end up measuring is dependent on internal scheduling, and you effectively end up measuring the time taken by the slowest promise, for all the promises. – mindplay.dk Apr 13 '22 at 09:29
6

Don't use setInterval to count milliseconds (It's inaccurate, lags, drifts and has a minimum interval of about 4ms). Just get two timestamps before and after the execution.

function measureAsyncFunc(fn: () => Promise<any>): Promise<number> {
    const start = Date.now();
    return fn.catch(() => {}).then(() => {
        const end = Date.now();
        const elapsed = end-start;
        return elapsed;
    });
}

For higher accuracy, replace Date.now by performance.now.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
3

Have a look at timeFnPromise and the related test cases.

  • target function is wrapped and executed when the wrapped function is called
  • appends fulfillment / rejection handler to the underlying Promise that returns the target functions return value as "ret" and the elapsed time as "elapsedTime"
  • supports arguments by passing them through to the target function

Samples Usage:

const wrappedFn = timeFnPromise(aFunctionThatReturnsAPromise)

wrappedFn()
.then((values)=>{
  const {ret, elapsedTime} = values
  console.log(`ret:[${ret}] elapsedTime:[${elapsedTime}]`)
})

Also available via NPM module jschest.

Dan
  • 267
  • 2
  • 6
2

Here's a simple wrapper function I wrote. It returns a Promise (via the async keyword), and so you can just call it with your promise. I added the time value as a property to the response. If you cannot have that value in the response, then you would need to remove it afterwards.

const stopwatchWrapper = async (promise) => {
  const startTime = Date.now()
  const resp = await promise
  resp.executionTime = Date.now() - startTime
  return resp
}

const axiosPromise = stopwatchWrapper(axios(reqSelected))
const response = await axiosPromise
console.log(response.executionTime)
MattC
  • 5,874
  • 1
  • 47
  • 40
0

It would be good to clarify that the proposed approaches by toskv only work with the resolution of a single promise. If we want to use Promise.all() the time result it returns is wrong.

Here is an example with the code that toskv developed, but using Promise.all()

Measure with Promise.all()

If someone needs to measure the time it takes to execute each of the promises executed with a Promise.all() the approach that can be followed is to make use of the interceptors and do the time measurements there

tomiW
  • 1