Skip to content

Performance degradation in concurrent usage with Promise, Promise.all #766

@sya-ri

Description

@sya-ri

When implementing XML processing using Promise.all with fast-xml-parser, we encountered significant performance
degradation issues. After conducting benchmark tests, the results suggested that this could be related to how
fast-xml-parser handles concurrent operations, particularly with larger XML files.

Summary

When processing large XML files (e.g., 13MB) concurrently with Promise, Promise.all (10 at the same time), fast-xml-parser is significantly slower than xml2js.
In contrast, for small or lightweight files, fast-xml-parser performs well and even outperforms xml2js.
When running a single parse (no concurrency), fast-xml-parser shows very high throughput, meaning the slowdown only appears with concurrent or async usage.


Reproduction Code

sya-ri@172d530

A. Concurrent execution (13MB / 10 at once)

Executed as benchmark/XmlParserConcurrent.mjs.

"use strict";

import Benchmark from "benchmark";
import {XMLParser} from "../src/fxp.js";
import xml2js from "xml2js";
import fxpv3 from "fast-xml-parser";
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
    
// compatibility
const __dirname = dirname(fileURLToPath(import.meta.url));

const suite = new Benchmark.Suite("XML Parser concurrent benchmark");

import fs from "fs";
import path from "path";
const fileNamePath = path.join(__dirname, "../spec/assets/midsize.xml"); // 13MB
const xmlData = fs.readFileSync(fileNamePath).toString();
const promiseSize = 10;

const fxpParser = new XMLParser();
const fxpParserForOrderedJs = new XMLParser({preserveOrder: true});

suite
    .add("fxp v3", {
      defer: true,
      fn: function(deferred) {
        Promise.all(Array.from({ length: promiseSize }).map(async () =>
          fxpv3.parse(xmlData)
        )).then(() => deferred.resolve());
      }
    })
    .add("fxp", {
      defer: true,
      fn: function(deferred) {
        Promise.all(Array.from({ length: promiseSize }).map(async () =>
          fxpParser.parse(xmlData)
        )).then(() => deferred.resolve());
      }
    })
    .add("fxp - preserve order", {
      defer: true,
      fn: function(deferred) {
        Promise.all(Array.from({ length: promiseSize }).map(async () =>
          fxpParserForOrderedJs.parse(xmlData)
        )).then(() => deferred.resolve());
      }
    })
    .add('xml2js ', {
      defer: true,
      fn: function(deferred) {
        Promise.all(Array.from({ length: promiseSize }).map(async () =>
          xml2js.parseStringPromise(xmlData)
        )).then(() => deferred.resolve());
      }
    })

    .on("start", function() {
        console.log("Running Suite: " + this.name);
    })
    .on("error", function(e) {
        console.log("Error in Suite: " + this.name, e);
    })
    .on("abort", function(e) {
        console.log("Aborting Suite: " + this.name, e);
    })
    .on("complete", function() {
        for (let j = 0; j < this.length; j++) {
            console.log(this[j].name + " : " + this[j].hz + " requests/second");
        }
    })
    .run({"async": true});

B. Non-concurrent (wrapped in async)

Executed as benchmark/XmlParserConcurrent.mjs.

"use strict";

import Benchmark from "benchmark";
import {XMLParser} from "../src/fxp.js";
import xml2js from "xml2js";
import fxpv3 from "fast-xml-parser";
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
    
// compatibility
const __dirname = dirname(fileURLToPath(import.meta.url));

const suite = new Benchmark.Suite("XML Parser concurrent benchmark");

import fs from "fs";
import path from "path";
const fileNamePath = path.join(__dirname, "../spec/assets/midsize.xml"); // 13MB
const xmlData = fs.readFileSync(fileNamePath).toString();

const fxpParser = new XMLParser();
const fxpParserForOrderedJs = new XMLParser({preserveOrder: true});

suite
    .add("fxp v3", {
      defer: true,
      fn: function(deferred) {
        (async () =>
          fxpv3.parse(xmlData)
        )().then(() => deferred.resolve());
      }
    })
    .add("fxp", {
      defer: true,
      fn: function(deferred) {
        (async () =>
          fxpParser.parse(xmlData)
        )().then(() => deferred.resolve());
      }
    })
    .add("fxp - preserve order", {
      defer: true,
      fn: function(deferred) {
        (async () =>
          fxpParserForOrderedJs.parse(xmlData)
        )().then(() => deferred.resolve());
      }
    })
    .add('xml2js ', {
      defer: true,
      fn: function(deferred) {
        (async () =>
          xml2js.parseStringPromise(xmlData)
        )().then(() => deferred.resolve());
      }
    })

    .on("start", function() {
        console.log("Running Suite: " + this.name);
    })
    .on("error", function(e) {
        console.log("Error in Suite: " + this.name, e);
    })
    .on("abort", function(e) {
        console.log("Aborting Suite: " + this.name, e);
    })
    .on("complete", function() {
        for (let j = 0; j < this.length; j++) {
            console.log(this[j].name + " : " + this[j].hz + " requests/second");
        }
    })
    .run({"async": true});

Benchmark Results

13MB XML / 10 concurrent

fxp v3 : 0.2865043014339054 requests/second
fxp : 0.16876260944496088 requests/second
fxp - preserve order : 0.1669240166670523 requests/second
xml2js : 1282.977050682912 requests/second

With large XML files under concurrency, fast-xml-parser is drastically slower than xml2js.


1.5KB XML / 10 concurrent

fxp v3 : 3238.1802137107325 requests/second
fxp : 1778.4294041322637 requests/second
fxp - preserve order : 2043.0091729760097 requests/second
xml2js  : 1745.291494844131 requests/second

With small XML, fast-xml-parser performs on par or better than xml2js.


CDATA (lightweight XML) / 10 concurrent

fxp v3 : 9789.883828979968 requests/second
fxp : 6691.052733490235 requests/second
fxp - preserve order : 7227.875838621091 requests/second
xml2js  : 3116.079812611551 requests/second

With lightweight files, fast-xml-parser outperforms xml2js significantly.


13MB XML / single execution (npm run parser)

fxp v3 : 100200.12823920241 requests/second
fxp : 66284.89077189047 requests/second
fxp - preserve order : 71015.28486649034 requests/second
xmlbuilder2 : 29305.972887978354 requests/second
xml2js  : 31336.192966600825 requests/second

In single execution, fast-xml-parser shows excellent performance, faster than xml2js.


13MB XML / non-concurrent (async wrapper)

fxp v3 : 2.807664801903926 requests/second
fxp : 1.8445207259077958 requests/second
fxp - preserve order : 2.0789000186976128 requests/second
xml2js : 13684.611351341111 requests/second

Even with a simple async wrapper, fast-xml-parser slows down drastically, while xml2js maintains good performance.


Notes

  • Compared parsers: fast-xml-parser and xml2js.
    xmlbuilder2 is excluded except in the single execution test.
  • Using xml2js.parseString(xmlData, callback) instead of xml2js.parseStringPromise(xmlData) produced the same results.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions