4
\$\begingroup\$

I am using the music metadata-browser npm package to retrieve audio metadata from files. This library is being used in an Electron and React desktop app.

To get the metadata of audio files, (and add it to the redux state), I use the following function to get metadata and add the file to redux state. This method works well with small amounts of data, but gets really slow (obviously) as more audio files are given to be processed. Is there a better way I can process these files? Not sure if javascript has any worker/job techniques I could use.

import * as mm from 'music-metadata-browser';

const addFile = (filePath, file, dispatch, ext) => {
  if (Buffer.isBuffer(file)) {
    mm.parseBuffer(file, ext)
      .then(metadata => {
        const libraryEntry = createLibraryEntry(filePath, metadata);

        dispatch({ type: constants.ADD_FILE, libraryEntry, totalFiles });
      });
  }
  else {
    mm.parseBlob(file, ext)
      .then((metadata) => {
        const libraryEntry = createLibraryEntry(filePath, metadata);

        dispatch({ type: constants.ADD_FILE, libraryEntry, totalFiles });
      });
  }
};

const processFiles = (files, dirPath, dispatch) => {
  files.map((file, index) => {
    const parsedFile = file.split('.');
    const format = parsedFile[parsedFile.length - 1];
    const filePath = `${dirPath}/${file}`;
    const isDirectory = fs.lstatSync(filePath).isDirectory();
    if (isDirectory) {
      fs.readdir(filePath, (err, files) => {processFiles(files, filePath, dispatch);});
    }
    else if (isValidFormat(format)) {
      fs.readFile(filePath, (err, file) => {
        addFile(filePath, Buffer(file), dispatch, format);
      });
    }
    return;
  });
};

Thanks for the help.

\$\endgroup\$
2
\$\begingroup\$

It appears music-metadata is the Node.js/desktop flavor of music-metadata-browser (intended for browsers). The author doesn't explain the differences between the two, but maybe the desktop variant will be faster for you.

Synchronous anything is a mistake for an inherently parallel task like this. Use fs.lstat instead of the fs.lstatSync. If createLibraryEntry or dispatch touch the disk they should be replaced with async versions as well.

Worker threads are a thing but you probably don't need them, because disk access is what's slowing you down and async disk access doesn't require threads.

If you have disk I/O that can't be asynchronized (if that's a word) by conventional callback functions, or if you've made everything async and it's still too slow, that is the time to consider using workers.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the response! Yes, I tried to use music-metadata but for some reason I'd receive an error saying fs is undefined when I tried to use it within my electron-based app. I'll definitely switch to using the async options and see how that impacts performance. \$\endgroup\$
    – Megan
    Apr 10 '19 at 15:40
  • \$\begingroup\$ Update: I ended up sending the files to the main electron process so that I could use music-metadata without any fs issues. (Also for speed and performance) \$\endgroup\$
    – Megan
    Apr 11 '19 at 18:25
1
\$\begingroup\$

Caching and Streaming.

Cache data

I can not workout if you are doing this each time your app loads. If you are and it is a slow point you should consider storing the processed data in a file or IndexedDB. The index can be the file path or hash derived from the file path.

Then the app should only retrieve the meta data from the cache as needed.

System RAM

You are loading the entire music file via the fs.readFile before you extract the meta data, and you read all the files in one go.

This will be a slow point, not due to the processing speed but rather the paging speed of system memory.

As example using my music directory. It is 104Gb of disk space. Your processFiles function would thus attempt to load 104Gb of music files. That will keep the paging system very busy and slow the whole system. (Personally if I saw an app abuse my system like that I would shut it down and uninstall)

Streaming

Looking at the metadata reader it does support streaming. You can then process the files without the need to load a massive RAM buffer of files.

mm.parseNodeStream(readableStream) will use Nodes readable stream

This will reduce the RAM overhead. However that still is a lot of data to process so it is still best to store the extracted meta data in some form of indexedDB

Then at start up scan the music directory for changes, and read the meta data only from new or updated files, all others will be in the cache.

Slow it down.

Last point is that you should not try to get the data as fast as possible. The job will take time, people are prepared to wait for full functionality as long as you don't block the interface while you process the data.

Prioritize if you can the files that need to be process (eg in UI's view, next to play, or whatever your app does).

Keep an eye on the systems level of activity and process fast when not very busy, and slowly when busy (Note be aware your processing will effect the system activity)

If it is a must that you process all file before the app can work then make the first processing job as part of the install.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the response! The idea is to only load in the files once, and then save state upon the app closing. I'm new to streams but I'll try to implement what I'm doing using mm.parseNodeStream(readableStream). I'll also look into indexedDB \$\endgroup\$
    – Megan
    Apr 10 '19 at 15:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.