Bug 189337 - decodeAudioData memory garbage collected too late
Summary: decodeAudioData memory garbage collected too late
Status: NEW
Alias: None
Product: WebKit
Classification: Unclassified
Component: Web Audio (show other bugs)
Version: Other
Hardware: iPhone / iPad iOS 11
: P2 Minor
Assignee: Nobody
URL:
Keywords: InRadar
Depends on:
Blocks:
 
Reported: 2018-09-06 05:58 PDT by ae
Modified: 2018-09-11 11:27 PDT (History)
3 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description ae 2018-09-06 05:58:23 PDT
When using decodeAudioData repeatedly with large files (or files that result in a large uncompressed buffer), it takes a very long time until the memory used by decodeAudioData is freed.

I use decodeAudioData to decode an audio file and then just get an overview (peaks) from it, which is only a few kilobytes. I do not hold any references to the buffer, channel data, or file after doing this.

Yet, potentially several hundred MBs of memory are being held by the "Page" process for around one minute after decodeAudioData, and then finally get freed.

Can't this large amount of data be collected earlier?
Comment 1 ae 2018-09-06 06:11:25 PDT
Note: I'm asking because I'm using a WKWebView in an iOS app, and it will randomly get killed (it turns blank) by the OS because it appearently uses too much memory.
Comment 2 Radar WebKit Bug Importer 2018-09-08 23:48:35 PDT
<rdar://problem/44271160>
Comment 3 Simon Fraser (smfr) 2018-09-10 14:05:12 PDT
I wonder if WebAudio-related classes are not reporting their memory costs to GC correctly?
Comment 4 Jer Noble 2018-09-10 16:15:16 PDT
(In reply to Simon Fraser (smfr) from comment #3)
> I wonder if WebAudio-related classes are not reporting their memory costs to
> GC correctly?

AudioBuffer (which contains all the decoded audio data) implements memoryCost(), which should be what the GC uses to determine object size. I'm guessing that there's an AudioBufferSourceNode which is retaining the decoded AudioBuffer and that the ABSN is being kept alive long after its playback has completed.
Comment 5 ae 2018-09-10 16:27:54 PDT
(In reply to Jer Noble from comment #4)
> I'm
> guessing that there's an AudioBufferSourceNode which is retaining the
> decoded AudioBuffer and that the ABSN is being kept alive long after its
> playback has completed.

As I said, I do not play back the buffer. I just use it for generating an overview. Here's the complete code (it's Tea, our in-house programming language that transcompiles to JS, but I hope it's readable enough!)

HTTP.get("sevenapp:///#{encodeURIComponent(@playingPath)}", (audioData) =>
	if not audioData?
		@clearPeaks()
		<-
	Audio.context.decodeAudioData(audioData, (buffer) =>
		if not buffer?
			@clearPeaks()
			<-
		samples = buffer.getChannelData(0)
		# 30 samples per second
		step = buffer.sampleRate / 30
		@peaks[@playingPath] = peaks = []
		for frame in [0...samples.length/step]
			sum = 0
			for index in [frame*step...(frame+1)*step] by 256 # accuracy, higher = worse, 1 = best
				if index >= samples.length
					break
				sum += Math.abs(samples[Math.floor(index)])
			sum /= step/256
			peaks.push(sum)
		@renderPeaks()
	)
)
Comment 6 Jer Noble 2018-09-11 11:09:50 PDT
(In reply to ae from comment #5)
> (In reply to Jer Noble from comment #4)
> > I'm
> > guessing that there's an AudioBufferSourceNode which is retaining the
> > decoded AudioBuffer and that the ABSN is being kept alive long after its
> > playback has completed.
> 
> As I said, I do not play back the buffer. I just use it for generating an
> overview. Here's the complete code (it's Tea, our in-house programming
> language that transcompiles to JS, but I hope it's readable enough!)
> 
> HTTP.get("sevenapp:///#{encodeURIComponent(@playingPath)}", (audioData) =>
> 	if not audioData?
> 		@clearPeaks()
> 		<-
> 	Audio.context.decodeAudioData(audioData, (buffer) =>
> 		if not buffer?
> 			@clearPeaks()
> 			<-
> 		samples = buffer.getChannelData(0)

Unless your in-house transpiler adds scoping statements to these variables, your going to be setting window.samples to a large Float32Array.  If your in-house transpiler /is/ adding scoping statements, it would be much more useful to post the transpiled source.
Comment 7 ae 2018-09-11 11:27:40 PDT
(In reply to Jer Noble from comment #6)
> (In reply to ae from comment #5)
> > (In reply to Jer Noble from comment #4)
> > > I'm
> > > guessing that there's an AudioBufferSourceNode which is retaining the
> > > decoded AudioBuffer and that the ABSN is being kept alive long after its
> > > playback has completed.
> > 
> > As I said, I do not play back the buffer. I just use it for generating an
> > overview. Here's the complete code (it's Tea, our in-house programming
> > language that transcompiles to JS, but I hope it's readable enough!)
> > 
> > HTTP.get("sevenapp:///#{encodeURIComponent(@playingPath)}", (audioData) =>
> > 	if not audioData?
> > 		@clearPeaks()
> > 		<-
> > 	Audio.context.decodeAudioData(audioData, (buffer) =>
> > 		if not buffer?
> > 			@clearPeaks()
> > 			<-
> > 		samples = buffer.getChannelData(0)
> 
> Unless your in-house transpiler adds scoping statements to these variables,
> your going to be setting window.samples to a large Float32Array.  If your
> in-house transpiler /is/ adding scoping statements, it would be much more
> useful to post the transpiled source.


Oh sorry, of course it makes 'samples' local (var samples), I'm just so used to this that I forgot. Here's the transpiled code (not too readable):

    getPeaks: function() {
      element('player-peaks').classList.remove('show');
      if (this.peaks[this.playingPath] != null) {
        timer(0.5, (function(_this) {
          return function() {
            _this.renderPeaks();
          };
        })(this));
        return;
      }
      if (Audio.context == null) {
        this.needPeaksOnResume = true;
        return;
      }
      this.needPeaksOnResume = false;
      HTTP.get("sevenapp:///" + (encodeURIComponent(this.playingPath)), (function(_this) {
        return function(audioData) {
          if (audioData == null) {
            _this.clearPeaks();
            return;
          }
          Audio.context.decodeAudioData(audioData, function(buffer) {
            var frame, i, index, j, peaks, ref, ref1, ref2, samples, step, sum;
            if (buffer == null) {
              _this.clearPeaks();
              return;
            }
            samples = buffer.getChannelData(0);
            step = buffer.sampleRate / 30;
            _this.peaks[_this.playingPath] = peaks = [];
            for (frame = i = 0, ref = samples.length / step; 0 <= ref ? i < ref : i > ref; frame = 0 <= ref ? ++i : --i) {
              sum = 0;
              for (index = j = ref1 = frame * step, ref2 = (frame + 1) * step; j < ref2; index = j += 256) {
                if (index >= samples.length) {
                  break;
                }
                sum += Math.abs(samples[Math.floor(index)]);
              }
              sum /= step / 256;
              peaks.push(sum);
            }
            _this.renderPeaks();
          });
        };
      })(this));
    },