Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tone.js stops playing if too many events are scheduled (Context.clearTimeout is a performance hog) #1168

Open
ContraTrouble opened this issue Feb 5, 2023 · 3 comments

Comments

@ContraTrouble
Copy link

I know this might sound as an unusual issue but please bear with me.

I'm generating music, using WAV samples for the instruments.

The generated music goes above 150 measures/bars of 4/4. The actual number of notes is around ~15,000-20,000 notes (1200 measures of 4/4 subdivided in triplets). I'm doing all music math, not using Tone's music primitives (measures, beats, subdivisions, anything).

When I play the music, close to say the 150th measure, Tone.js starts missing to play some instrument sounds, and above 155th measure, it stops playing anything. The callbacks to play the sounds continue to be invoked, so it hasn't crashed internally but it continuously slows down to almost a halt.

When I ran Chrome's Performance tools, I noticed two things taking most of the time:

  1. Context.clearTimeout, which looks like this:
    /**
     * Clears a previously scheduled timeout with Tone.context.setTimeout
     * @param  id  The ID returned from setTimeout
     */
    clearTimeout(id) {
        this._timeouts.forEach((event) => {
            if (event.id === id) {
                this._timeouts.remove(event);
            }
        });
        return this;
    }

If I put a break on the first line, I can see thousands or tens of thousands of events.
This function is used internally and the call stack is around restarting a sound (Player).

  1. Timeline._iterate which looks like this:
    /**
     * Internal iterator. Applies extra safety checks for
     * removing items from the array.
     */
    _iterate(callback, lowerBound = 0, upperBound = this._timeline.length - 1) {
        this._timeline.slice(lowerBound, upperBound + 1).forEach(callback);
    }
    /**

When there are tens of thousands of scheduled events in the timeline, the above two functions eat the entire time.

To Reproduce

I don't have a codesandbox but can create one if you need me to - all that's needed is a lot of notes to be played. Can't share my code, it's too involved.

Expected behavior

Tone.js should continue playing.

What I've tried

I've tried reducing the number of measures. It does work with less measures (cancelTimeout still takes most of the time), although it really depends on the number of notes/sounds that get scheduled to be played, not the measures.

Additional context

Caching the timeouts by ID in a Map or Object would clearly help, although it would obviously increase the needed memory to store them.

I'm not sure why _iterate always slices the array but judging from the comment, a callback might remove and event from the timeline. This could be solved in several ways but I need to spend quite some time understanding and debugging the codebase before I'm comfortable to propose a solution. But obviously, copying the array every time doesn't look a great solution either.

One possible solution, that's a bit involved is to start chunking measures, and start scheduling playing every next K measures, when K-1 measures have been already played. This, I guess, would decrease the timeline's length significantly. I'm really not looking forward to doing that :)

In any case, I'm really willing to do a lot more work on figuring how to deal with the above issue because it's a showstopper for me but can't happen in the next few months (am quite busy).

Thanks for reading all of this :)

@FractalHQ
Copy link

This has been my main problem for a couple months now. I've spent a lot of time refactoring my code in search of memory leaks and ways to minimize the burden on the GC. Ultimately, it seem sees the biggest leaks are coming from Tones event system.

I'll be following this closely in hopes of gaining more insight on how we might be able to improve the internals and alleviate some bottlenecks!

@ceelian
Copy link

ceelian commented Feb 14, 2023

Nice analysis! Are you creating 15.000-20.000 events with context.setTimeout()? I am not sure if I understand your approach completely but might it be an option to work with Tone.Loop instead or any other more resource efficient approach like working with fixed on grid events and have some micro timing adjustments for special cases for the scheduler? Haven't tested it but it might be more efficient as the callback can be reused and if you are pre-calculating the timings for all the notes on "your side" anyway that might be an option.

@ContraTrouble
Copy link
Author

I'm using Transport.scheduleOnce where I play a sound using Player.start - and yes, it can be tens of thousands of notes distributed in hundreds of sections with multiple measures each.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants