Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

base64 encodeByteArray is very slow #1161

Closed
yasushi-saito opened this issue Mar 29, 2022 · 3 comments
Closed

base64 encodeByteArray is very slow #1161

yasushi-saito opened this issue Mar 29, 2022 · 3 comments
Labels
sunset Issues/PRs auto-closed when repo was archived

Comments

@yasushi-saito
Copy link

(reposted from grpc/grpc-web#1187)

TL; DR: grpc-web uses goog.crypt.base64.encodeByteArray to encode the payload. This function can only encode at the throughput of ~16MB/s. js-base64 package is 50x faster, so rewriting encodeByteArray using ideas from js-base64 (or other fast base64 package) is greatly appreciated.

Background: we have a grpc-web-based app that performs bulk data upload, and it can sustain only about ~15MB/s regardless of the underlying network speed. The reason is that the base64 encoder used by grpc-web (goog.crypt.base64) is quite slow. I did a quick benchmark between goog.crypt.base64.encodeByteArray vs Base64.fromUint8Array (from https://www.npmjs.com/package/js-base64), and the former is about 50x slower than js-base64. E.g., encoding a random 16MB Uint8Array payload takes about ~1000ms with goog.crypt.base64, 19ms with js-base64.

FYI, here's a node script that I used.

require('google-closure-library');
goog.require('goog.crypt.base64');
const {Base64} = require('js-base64');

// Create a Uint8Array with random contents.
function randomArray(len) {
    const a = new Uint8Array(len);
    for (let i = 0; i < len; i += 1) {
        a[i] = Math.ceil((Math.random()) * 256);
    }
    return a;
}

function runBench(label, len, encode, decode) {
    // Run a encode/decode roundtrip once and verify their behavior.
    const bin = randomArray(len);
    const str = encode(bin) // base64 string
    const bin1 = decode(str); // should be the same as bin
    if (bin1.length != bin.length) {
        throw Error(`wrong length: got ${bin1.length} want ${bin.length}`);
    }
    for (let i = 0; i < bin.length; i += 1) {
        if (bin[i] != bin1[i]) {
            throw Error(`wrong data at ${i}: got ${bin1[i]} want ${bin[i]}`);
        }
    }

    // Run a given function in a loop and measure a mean runtime.
    const run = (cb) => {
        const startTime = new Date();
        let rep = 0;
        for (; ;) {
            for (let i = 0; i < 10; i += 1) {
                cb();
                rep += 1;
            }
            const elapsed = new Date() - startTime;
            if (elapsed > 1000) {
                return elapsed / rep;
            }
        }
    };

    const encodeTime = run(() => encode(bin));
    console.log(`${label} encode: len=${len}: ${encodeTime}ms`);
    const decodeTime = run(() => decode(str));
    console.log(`${label} decode: len=${len}: ${decodeTime}ms`);
}

const lens = [8 << 10, 1 << 20, 16 << 20];
for (i in lens) {
    runBench("jsbase64", lens[i],
             Base64.fromUint8Array,
             Base64.toUint8Array);
    runBench("googcrypt", lens[i],
             goog.crypt.base64.encodeByteArray,
             goog.crypt.base64.decodeStringToUint8Array);
}

result:

jsbase64 encode: len=8192: 0.008025978191148172ms
jsbase64 decode: len=8192: 0.08218390804597701ms
googcrypt encode: len=8192: 0.15714285714285714ms
googcrypt decode: len=8192: 0.27424657534246577ms
jsbase64 encode: len=1048576: 0.997029702970297ms
jsbase64 decode: len=1048576: 10.55ms
googcrypt encode: len=1048576: 57.2ms
googcrypt decode: len=1048576: 35.46666666666667ms
jsbase64 encode: len=16777216: 18.333333333333332ms
jsbase64 decode: len=16777216: 178.3ms
googcrypt encode: len=16777216: 966.8ms
googcrypt decode: len=16777216: 581.6ms
@sampajano
Copy link
Contributor

Thanks for the report, @yasushi-saito! Had a conversation with Closure team and we'll aim to provide an update here soon. Thanks! :)

@shicks
Copy link
Member

shicks commented Aug 4, 2022

I just tested this. Indeed, there's a 50x speed difference in NodeJS, but on the browser, it's much more modest - only a factor of 2 or so. This is because js-base64 "cheats" by using Buffer to call directly into the native C++ base64 implementation. This is of course impossible in the browser, and since Closure Library only officially supports the browser use case, it's unlikely we'd look to add any special optimizations just for node.

Does grpc-web have a use case in the browser that's still seeing poor performance?

@shicks shicks added the sunset Issues/PRs auto-closed when repo was archived label Aug 1, 2024
@shicks
Copy link
Member

shicks commented Aug 1, 2024

This issue is being closed as a preparation step before archiving the repository. See the README for more detail.

@shicks shicks closed this as not planned Won't fix, can't repro, duplicate, stale Aug 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
sunset Issues/PRs auto-closed when repo was archived
Projects
None yet
Development

No branches or pull requests

3 participants