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

perf: support sign message in batch #225

Merged
merged 1 commit into from
Apr 17, 2023
Merged

Conversation

zhangyouxin
Copy link
Contributor

@zhangyouxin zhangyouxin commented Apr 17, 2023

What Changed

This PR resolve #222

Motivation

It takes too long to sign a big transaction containing many inputs with different locks.

For example, on my machine, it takes about 1 minute to sign a transaction with 30 inputs.

Currently it's because every signature is generated separately, I have tried to figure out which step we can optimize by logging the time elapsed:

// KeystoreService.ts
signMessage: async (payload: SignMessagePayload): Promise<HexString> => {
+   const startTime = Date.now();
    const keystoreData = await resolveKeystoreData();
+   const step1 = Date.now();

    const keystore = Keystore.fromJson(keystoreData.wss);
+   const step2 = Date.now();

    const extendedPrivateKey = keystore.extendedPrivateKey(await resolveValue(payload.password));
+   const step3 = Date.now();

    const result =  key.signRecoverable(
      bytes.hexify(payload.message),
      extendedPrivateKey.privateKeyInfoByPath(payload.path).privateKey,
    );
+   const step4 = Date.now();
+   console.log('resolve keystore service time:', step1 - startTime);
+   console.log('read keystore from json:', step2 - step1);
+   console.log('extend privatekey:', step3 - step2);
+   console.log('sign:', step4 - step3);
    return result;
}

and the sample result:

keystore.ts:113 resolve keystore service time: 13
keystore.ts:114 read keystore from json: 0
keystore.ts:115 extend privatekey: 1856
keystore.ts:116 sign: 22

keystore.ts:113 resolve keystore service time: 18
keystore.ts:114 read keystore from json: 0
keystore.ts:115 extend privatekey: 1853
keystore.ts:116 sign: 8

which means getting extendedPrivateKey takes most of time, by batching the sign requests,
we just need to generate extendedPrivateKey once in signing one tx, much improving the performance when signing a tx with multiple inputs.

Benchmark Results:

sign 1 input transaction x 0.54 ops/sec ±0.16% (52 runs sampled)
sign 5 input transaction x 0.38 ops/sec ±0.17% (51 runs sampled)
sign 10 input transaction x 0.27 ops/sec ±0.11% (51 runs sampled)
sign 20 input transaction x 0.18 ops/sec ±0.09% (50 runs sampled)

sign 1 input transaction without batch x 0.53 ops/sec ±0.82% (52 runs sampled)
sign 5 input transaction without batch x 0.11 ops/sec ±0.07% (50 runs sampled)
sign 10 input transaction without batch x 0.05 ops/sec ±0.26% (50 runs sampled)
sign 20 input transaction without batch x 0.03 ops/sec ±0.36% (50 runs sampled)

As expected, the performance is not significantly improved when there is only one cell in the tx inputs, but it improves about 500% when there are 20 cell inputs in the tx.

benchmark.ts
import Benchmark from "benchmark";
const suite = new Benchmark.Suite();

const signNInputTx = (inputCount: number): Promise<string[]> => {
  return service.signMessage({
    messageInfos: generateMessageInfo(inputCount),
    password: MOCK_PLATFORM_PASSWORD,
  }) as Promise<string[]>;
};

const signNInputTxWithOutBatch = (inputCount: number) => {
  const messageInfos = generateMessageInfo(inputCount);
  return Promise.all(messageInfos.map(messageInfo => service.signMessage({
    messageInfos: [messageInfo],
    password: MOCK_PLATFORM_PASSWORD,
  }) as Promise<string[]>))
};

const generateMessageInfo = (count: number) => {
  return new Array(count).fill(null).map((_, index) => {
    return { path: `m/44'/309'/0'/0/${index}`, message: `0x${'00'.repeat(32)}` }
  })
}

Benchmark.options.minSamples = 50;

async function runBenchmark(): Promise<void> {
  return new Promise((resolve) => {
    suite
      .add('sign 1 input transaction', {
        defer: true,
        fn: function (deferred: any) {
          signNInputTx(1).then(res => {
            deferred.resolve();
          })
        },
      })
      .add('sign 5 input transaction', {
        defer: true,
        fn: function (deferred: any) {
          signNInputTx(5).then(res => {
            deferred.resolve();
          })
        },
      })
      .add('sign 10 input transaction', {
        defer: true,
        fn: function (deferred: any) {
          signNInputTx(10).then(res => {
            deferred.resolve();
          })
        },
      })
      .add('sign 20 input transaction', {
        defer: true,
        fn: function (deferred: any) {
          signNInputTx(20).then(res => {
            deferred.resolve();
          })
        },
      })
      .add('sign 1 input transaction without batch', {
        defer: true,
        fn: function (deferred: any) {
          signNInputTxWithOutBatch(1).then(res => {
            deferred.resolve();
          })
        },
      })
      .add('sign 5 input transaction without batch', {
        defer: true,
        fn: function (deferred: any) {
          signNInputTxWithOutBatch(5).then(res => {
            deferred.resolve();
          })
        },
      })
      .add('sign 10 input transaction without batch', {
        defer: true,
        fn: function (deferred: any) {
          signNInputTxWithOutBatch(10).then(res => {
            deferred.resolve();
          })
        },
      })
      .add('sign 20 input transaction without batch', {
        defer: true,
        fn: function (deferred: any) {
          signNInputTxWithOutBatch(20).then(res => {
            deferred.resolve();
          })
        },
      })
      .on("cycle", function (event: any) {
        console.log(String(event.target));
      })
      .run({ async: true });
  });
}

await runBenchmark();

Change Type

Indicate the type of change your pull request is:

  • documentation
  • patch
  • minor
  • major

@codecov
Copy link

codecov bot commented Apr 17, 2023

Codecov Report

Merging #225 (581627e) into main (b8a6205) will increase coverage by 0.04%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main     #225      +/-   ##
==========================================
+ Coverage   86.12%   86.17%   +0.04%     
==========================================
  Files          45       45              
  Lines         872      875       +3     
  Branches      114      114              
==========================================
+ Hits          751      754       +3     
  Misses         20       20              
  Partials      101      101              
Impacted Files Coverage Δ
...extension-chrome/src/services/keystore/keystore.ts 85.24% <100.00%> (+0.24%) ⬆️
...ion-chrome/src/services/ownership/fullOwnership.ts 85.71% <100.00%> (+0.38%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

Copy link
Contributor

@homura homura left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes of this PR looks fine to me, but maybe the performance testing could be better

var suite = new Benchmark.Suite;

// add tests
suite.add('without batch', function(deffered) {
  Promisela.all(messages.map(oldService.signMessage))
    .then(deffered.resolve());
})
.add('with batch', function() {
  newService.signMessage({messageInfo: [], password: '...'})
	.then(deffered.resolve)
})
// add listeners
.on('cycle', function(event) {
  console.log(event.target)
})
.run({async: true});

@homura homura added the performance Improve performance of an existing feature label Apr 17, 2023
@zhangyouxin
Copy link
Contributor Author

The changes of this PR looks fine to me, but maybe the performance testing could be better

var suite = new Benchmark.Suite;

// add tests
suite.add('without batch', function(deffered) {
  Promisela.all(messages.map(oldService.signMessage))
    .then(deffered.resolve());
})
.add('with batch', function() {
  newService.signMessage({messageInfo: [], password: '...'})
	.then(deffered.resolve)
})
// add listeners
.on('cycle', function(event) {
  console.log(event.target)
})
.run({async: true});

Added benchmark report

@homura homura changed the title refactor: batch sign message with keystore service perf: support sign message in batch Apr 17, 2023
@homura homura merged commit 9056fe8 into ckb-js:main Apr 17, 2023
@github-actions
Copy link

🚀 PR was released in v0.0.14 🚀

@github-actions github-actions bot added the released This issue/pull request has been released. label Apr 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Improve performance of an existing feature released This issue/pull request has been released.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

batch signMessage in keystore to optimize performance
2 participants