Skip to content

fix(subject): Faster subscriptions management in Subject#7240

Merged
benlesh merged 2 commits intoReactiveX:masterfrom
dubzzz:faster-subjects-unsub-2
Apr 4, 2023
Merged

fix(subject): Faster subscriptions management in Subject#7240
benlesh merged 2 commits intoReactiveX:masterfrom
dubzzz:faster-subjects-unsub-2

Conversation

@dubzzz
Copy link
Contributor

@dubzzz dubzzz commented Mar 30, 2023

Description:

The current implementation of the Subject was backing itself on an array of observers. When one observer unsubscribed itself, it was performing a linear scan on the array of observers and dropping the item from the array of observers. The algorithm complexity of this operation was O(n) with n being the number of observers connected to the Subject.

It caused code like:

const allSubscriptions = [];
const source = new Subject();
for (let index = 0 ; index !== 1_000_000 ; ++index) {
  allSubscriptions.push(source.subscribe()); // rather quick
}
for (const subscription of allSubscriptions) {
  subscription.unsubscribe(); // taking very long
}

The proposed approach consists into changing our backing collection (an array) into a Map. But we lose somehow the set capability on subject.observers (might possibly be patched in some way).

The command cross-env TS_NODE_PROJECT=tsconfig.mocha.json mocha --config spec/support/.mocharc.js "spec/**/Subje*-spec.ts" with the new test added:

  • Now: 497ms
  • Before: 10s

BREAKING CHANGE:

Subject.observers has been impacted. If needed we can probably make it backward compatible in some ways. Here is what changed:

  • Now a getter property, was previously a basic property
  • The value change from one call to another -we recompute it anytime we access it)
  • There is no setter anymore
  • The value is never null

Related issue (if exists):

The current implementation of the Subject was backing itself on an array of observers. When one observer unsubscribed itself, it was performing a linear scan on the array of observers and dropping the item from the array of observers. The algorithm complexity of this operation was O(n) with n being the number of observers connected to the Subject.

It caused code like:

```js
const allSubscriptions = [];
const source = new Subject();
for (let index = 0 ; index !== 1_000_000 ; ++index) {
  allSubscriptions.push(source.subscribe()); // rather quick
}
for (const subscription of allSubscriptions) {
  subscription.unsubscribe(); // taking very long
}
```

The proposed approach consists into changing our backing collection (an array) into a Map. But we lose somehow the set capability on subject.observers (might possibly be patched in some way).

Following that change the command `cross-env TS_NODE_PROJECT=tsconfig.mocha.json mocha --config spec/support/.mocharc.js "spec/**/Subje*-spec.ts"` passed to 452ms from 10s.
@dubzzz
Copy link
Contributor Author

dubzzz commented Mar 30, 2023

By the way I started to run some benchmarks against the new code and the master I took my branch from.

Here is the code I used
// @ts-check
const { Bench } = require('tinybench');
const { Subject: SubjectMaster } = require('./dist-master/cjs');
const { Subject: SubjectPr } = require('./dist/cjs');

async function run() {
  const numIterations = 10_000;
  const bench = new Bench({ warmupIterations: Math.ceil(numIterations / 20), iterations: numIterations });

  bench.add('[master] Creating Subject', () => {
    new SubjectMaster();
  });
  bench.add('[pr] Creating Subject', () => {
    new SubjectPr();
  });

  bench.add('[master] 0 subscribed, 1 next', () => {
    const source = new SubjectMaster();
    source.next(1);
  });
  bench.add('[pr] 0 subscribed, 1 next', () => {
    const source = new SubjectPr();
    source.next(1);
  });

  bench.add('[master] 0 subscribed, 100 next', () => {
    const source = new SubjectMaster();
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
  });
  bench.add('[pr] 0 subscribed, 100 next', () => {
    const source = new SubjectPr();
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
  });

  bench.add('[master] 1 subscribed, 1 next', () => {
    const source = new SubjectMaster();
    const subscription = source.asObservable().subscribe();
    source.next(1);
    subscription.unsubscribe();
  });
  bench.add('[pr] 1 subscribed, 1 next', () => {
    const source = new SubjectPr();
    const subscription = source.asObservable().subscribe();
    source.next(1);
    subscription.unsubscribe();
  });

  bench.add('[master] 1 subscribed, 100 next', () => {
    const source = new SubjectMaster();
    const subscription = source.asObservable().subscribe();
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    subscription.unsubscribe();
  });
  bench.add('[pr] 1 subscribed, 100 next', () => {
    const source = new SubjectPr();
    const subscription = source.asObservable().subscribe();
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    subscription.unsubscribe();
  });

  bench.add('[master] 3 subscribed, 1 next', () => {
    const source = new SubjectMaster();
    const s1 = source.asObservable().subscribe();
    const s2 = source.asObservable().subscribe();
    const s3 = source.asObservable().subscribe();
    source.next(1);
    s1.unsubscribe();
    s2.unsubscribe();
    s3.unsubscribe();
  });
  bench.add('[pr] 3 subscribed, 1 next', () => {
    const source = new SubjectPr();
    const s1 = source.asObservable().subscribe();
    const s2 = source.asObservable().subscribe();
    const s3 = source.asObservable().subscribe();
    source.next(1);
    s1.unsubscribe();
    s2.unsubscribe();
    s3.unsubscribe();
  });

  bench.add('[master] 3 subscribed, 100 next', () => {
    const source = new SubjectMaster();
    const s1 = source.asObservable().subscribe();
    const s2 = source.asObservable().subscribe();
    const s3 = source.asObservable().subscribe();
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    s1.unsubscribe();
    s2.unsubscribe();
    s3.unsubscribe();
  });
  bench.add('[pr] 3 subscribed, 100 next', () => {
    const source = new SubjectPr();
    const s1 = source.asObservable().subscribe();
    const s2 = source.asObservable().subscribe();
    const s3 = source.asObservable().subscribe();
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    s1.unsubscribe();
    s2.unsubscribe();
    s3.unsubscribe();
  });

  bench.add('[master] 10 subscribed, 1 next', () => {
    const source = new SubjectMaster();
    const s = [];
    for (let i = 0; i !== 10; ++i) {
      s.push(source.asObservable().subscribe());
    }
    source.next(1);
    for (const e of s) {
      e.unsubscribe();
    }
  });
  bench.add('[pr] 10 subscribed, 1 next', () => {
    const source = new SubjectPr();
    const s = [];
    for (let i = 0; i !== 10; ++i) {
      s.push(source.asObservable().subscribe());
    }
    source.next(1);
    for (const e of s) {
      e.unsubscribe();
    }
  });

  bench.add('[master] 10 subscribed, 100 next', () => {
    const source = new SubjectMaster();
    const s = [];
    for (let i = 0; i !== 10; ++i) {
      s.push(source.asObservable().subscribe());
    }
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    for (const e of s) {
      e.unsubscribe();
    }
  });
  bench.add('[pr] 10 subscribed, 100 next', () => {
    const source = new SubjectPr();
    const s = [];
    for (let i = 0; i !== 10; ++i) {
      s.push(source.asObservable().subscribe());
    }
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    for (const e of s) {
      e.unsubscribe();
    }
  });

  bench.add('[master] 100 subscribed, 1 next', () => {
    const source = new SubjectMaster();
    const s = [];
    for (let i = 0; i !== 100; ++i) {
      s.push(source.asObservable().subscribe());
    }
    source.next(1);
    for (const e of s) {
      e.unsubscribe();
    }
  });
  bench.add('[pr] 100 subscribed, 1 next', () => {
    const source = new SubjectPr();
    const s = [];
    for (let i = 0; i !== 100; ++i) {
      s.push(source.asObservable().subscribe());
    }
    source.next(1);
    for (const e of s) {
      e.unsubscribe();
    }
  });

  bench.add('[master] 100 subscribed, 100 next', () => {
    const source = new SubjectMaster();
    const s = [];
    for (let i = 0; i !== 100; ++i) {
      s.push(source.asObservable().subscribe());
    }
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    for (const e of s) {
      e.unsubscribe();
    }
  });
  bench.add('[pr] 100 subscribed, 100 next', () => {
    const source = new SubjectPr();
    const s = [];
    for (let i = 0; i !== 100; ++i) {
      s.push(source.asObservable().subscribe());
    }
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    for (const e of s) {
      e.unsubscribe();
    }
  });

  bench.add('[master] 1000 subscribed, 1 next', () => {
    const source = new SubjectMaster();
    const s = [];
    for (let i = 0; i !== 1000; ++i) {
      s.push(source.asObservable().subscribe());
    }
    source.next(1);
    for (const e of s) {
      e.unsubscribe();
    }
  });
  bench.add('[pr] 1000 subscribed, 1 next', () => {
    const source = new SubjectPr();
    const s = [];
    for (let i = 0; i !== 1000; ++i) {
      s.push(source.asObservable().subscribe());
    }
    source.next(1);
    for (const e of s) {
      e.unsubscribe();
    }
  });

  bench.add('[master] 1000 subscribed, 100 next', () => {
    const source = new SubjectMaster();
    const s = [];
    for (let i = 0; i !== 1000; ++i) {
      s.push(source.asObservable().subscribe());
    }
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    for (const e of s) {
      e.unsubscribe();
    }
  });
  bench.add('[pr] 1000 subscribed, 100 next', () => {
    const source = new SubjectPr();
    const s = [];
    for (let i = 0; i !== 1000; ++i) {
      s.push(source.asObservable().subscribe());
    }
    for (let i = 0; i !== 100; ++i) {
      source.next(i);
    }
    for (const e of s) {
      e.unsubscribe();
    }
  });

  await bench.warmup();
  await bench.run();

  console.table(
    bench.tasks.map(({ name, result }) => {
      return {
        Name: name,
        Mean: result?.mean,
        P75: result?.p75,
        P99: result?.p99,
        RME: result?.rme,
      };
    })
  );
}
run();

Here are the results for the current version of the code:

┌─────────┬──────────────────────────────────────┬────────────────────────┬────────────────────────┬───────────────────────┬────────────────────┐
│ (index) │                 Name                 │          Mean          │          P75           │          P99          │        RME         │
├─────────┼──────────────────────────────────────┼────────────────────────┼────────────────────────┼───────────────────────┼────────────────────┤
│    0    │     '[master] Creating Subject'      │ 0.00015643814199720028 │ 0.00019999966025352478 │ 0.0003000004217028618 │ 11.589776920801196 │
│    1    │       '[pr] Creating Subject'        │ 0.00017207796397903874 │ 0.00019999966025352478 │ 0.0005000000819563866 │ 0.8590171745396832 │
│    2    │   '[master] 0 subscribed, 1 next'    │ 0.00020557814425434853 │ 0.0002000005915760994  │ 0.0005000000819563866 │ 1.3377641081389615 │
│    3    │     '[pr] 0 subscribed, 1 next'      │ 0.00018017834274192457 │ 0.0002000005915760994  │ 0.0005000000819563866 │ 0.7602265034322395 │
│    4    │  '[master] 0 subscribed, 100 next'   │ 0.0030202487889903437  │ 0.0029999995604157448  │ 0.005300000309944153  │ 0.590282723735076  │
│    5    │    '[pr] 0 subscribed, 100 next'     │ 0.0004527702810480123  │ 0.0005000000819563866  │ 0.0008999994024634361 │ 0.5861990278579561 │
│    6    │   '[master] 1 subscribed, 1 next'    │ 0.0008216253886610409  │ 0.0007999995723366737  │ 0.001600000075995922  │ 0.6719833246454169 │
│    7    │     '[pr] 1 subscribed, 1 next'      │  0.000856679151685021  │ 0.0008000005036592484  │ 0.0015000002458691597 │ 0.7766441850042467 │
│    8    │  '[master] 1 subscribed, 100 next'   │  0.004788513404096101  │  0.004699999466538429  │ 0.008899999782443047  │ 0.7056875628905762 │
│    9    │    '[pr] 1 subscribed, 100 next'     │ 0.0014018038008829956  │ 0.0013999994844198227  │ 0.0022999998182058334 │ 0.8684742543095227 │
│   10    │   '[master] 3 subscribed, 1 next'    │ 0.0018978834599352103  │ 0.0017999997362494469  │ 0.0034000007435679436 │ 3.032935309575753  │
│   11    │     '[pr] 3 subscribed, 1 next'      │ 0.0017642026010910529  │ 0.0016999999061226845  │ 0.003200000151991844  │ 0.5353075708343223 │
│   12    │  '[master] 3 subscribed, 100 next'   │  0.00802172429488669   │  0.007799999788403511  │  0.01489999983459711  │ 0.5660223830939138 │
│   13    │    '[pr] 3 subscribed, 100 next'     │  0.003131604704897833  │ 0.0030000004917383194  │ 0.005499999970197678  │ 1.134493567559593  │
│   14    │   '[master] 10 subscribed, 1 next'   │  0.004826806852278755  │  0.004499000497162342  │ 0.008899999782443047  │ 0.6422339682792549 │
│   15    │     '[pr] 10 subscribed, 1 next'     │  0.004576598058776545  │ 0.0044999998062849045  │ 0.0074000004678964615 │ 1.3101714281276127 │
│   16    │  '[master] 10 subscribed, 100 next'  │  0.020022055576250558  │  0.01850000023841858   │  0.03910000063478947  │ 0.6652153397296293 │
│   17    │    '[pr] 10 subscribed, 100 next'    │  0.009184353557200084  │  0.008299999870359898  │ 0.015599999576807022  │ 2.183817921324249  │
│   18    │  '[master] 100 subscribed, 1 next'   │  0.04320903015868642   │  0.038200000301003456  │  0.10059900023043156  │ 1.2233526482136148 │
│   19    │    '[pr] 100 subscribed, 1 next'     │  0.03559318009971064   │  0.03399999998509884   │  0.07059899996966124  │ 0.7623208783019286 │
│   20    │ '[master] 100 subscribed, 100 next'  │  0.17524525809325278   │   0.1589989997446537   │  0.36219900008291006  │ 0.6542612434974838 │
│   21    │   '[pr] 100 subscribed, 100 next'    │  0.07490052990522236   │  0.06799900066107512   │  0.15750000067055225  │ 0.8771896860089285 │
│   22    │  '[master] 1000 subscribed, 1 next'  │   1.5466663203015925   │   1.3917950000613928   │  3.0632890006527305   │ 1.1953427047583378 │
│   23    │    '[pr] 1000 subscribed, 1 next'    │   0.4354844354032539   │   0.3509990004822612   │  1.4115949999541044   │ 1.3006120132927175 │
│   24    │ '[master] 1000 subscribed, 100 next' │   2.8816406472022646   │   3.048589000478387    │   5.759379000402987   │  0.58972696843935  │
│   25    │   '[pr] 1000 subscribed, 100 next'   │   1.3074916874016635   │   1.176095999777317    │  2.6631909999996424   │ 0.7865946049284476 │
└─────────┴──────────────────────────────────────┴────────────────────────┴────────────────────────┴───────────────────────┴────────────────────┘
For previous version

It used for (const observer of this.snapshotedObservers) in next/error/complete.
Instead of for (let i = 0; i !== snapshotedObservers.length; ++i).

┌─────────┬──────────────────────────────────────┬────────────────────────┬───────────────────────┬───────────────────────┬────────────────────┐
│ (index) │                 Name                 │          Mean          │          P75          │          P99          │        RME         │
├─────────┼──────────────────────────────────────┼────────────────────────┼───────────────────────┼───────────────────────┼────────────────────┤
│    0    │     '[master] Creating Subject'      │ 0.00012953836437439974 │ 0.000100000761449337  │ 0.0003000004217028618 │ 0.8079907019422605 │
│    1    │       '[pr] Creating Subject'        │ 0.00017635711767006987 │ 0.0002000005915760994 │ 0.0004000002518296242 │ 0.8289423819308911 │
│    2    │   '[master] 0 subscribed, 1 next'    │ 0.00020459377653131344 │ 0.0002000005915760994 │ 0.0005000000819563866 │ 1.1378771813981523 │
│    3    │     '[pr] 0 subscribed, 1 next'      │ 0.00020221618217370962 │ 0.0002000005915760994 │ 0.0005000000819563866 │ 0.6810397390682086 │
│    4    │  '[master] 0 subscribed, 100 next'   │ 0.0029463214406694736  │ 0.0028999997302889824 │ 0.005300000309944153  │ 0.5257643196869795 │
│    5    │    '[pr] 0 subscribed, 100 next'     │  0.002991678455293176  │ 0.0028999997302889824 │ 0.006000000052154064  │ 0.5867012451076267 │
│    6    │   '[master] 1 subscribed, 1 next'    │ 0.0007954250431398286  │ 0.000700000673532486  │ 0.001600000075995922  │ 0.8766148496955309 │
│    7    │     '[pr] 1 subscribed, 1 next'      │ 0.0008213868146737953  │ 0.000700000673532486  │ 0.0016999999061226845 │ 1.1956179944223218 │
│    8    │  '[master] 1 subscribed, 100 next'   │  0.004908873666542814  │ 0.004700000397861004  │ 0.009000000543892384  │ 0.590427156123814  │
│    9    │    '[pr] 1 subscribed, 100 next'     │  0.004821027538623853  │ 0.004699999466538429  │ 0.008899999782443047  │ 1.346060178011175  │
│   10    │   '[master] 3 subscribed, 1 next'    │ 0.0017776441528556996  │ 0.001700000837445259  │ 0.0032999999821186066 │ 0.5591522908763634 │
│   11    │     '[pr] 3 subscribed, 1 next'      │  0.002093957002878406  │ 0.002199999988079071  │ 0.0035999994724988937 │  3.92953070123866  │
│   12    │  '[master] 3 subscribed, 100 next'   │  0.008743048678321383  │ 0.008100000210106373  │ 0.015899999998509884  │ 0.6343904943811376 │
│   13    │    '[pr] 3 subscribed, 100 next'     │  0.008495988327560293  │ 0.007900000549852848  │ 0.015999999828636646  │ 0.8790439256253967 │
│   14    │   '[master] 10 subscribed, 1 next'   │ 0.0043729048539165325  │  0.00430000014603138  │ 0.008100000210106373  │ 0.9333949527437183 │
│   15    │     '[pr] 10 subscribed, 1 next'     │ 0.0045709372217430525  │ 0.0044999998062849045 │ 0.008200000040233135  │ 0.5809235637555371 │
│   16    │  '[master] 10 subscribed, 100 next'  │  0.021834660001977563  │  0.01919999998062849  │ 0.042500000447034836  │ 0.7986149010500317 │
│   17    │    '[pr] 10 subscribed, 100 next'    │  0.019986633528925612  │ 0.019100000150501728  │  0.03870000038295984  │ 0.609131840525482  │
│   18    │  '[master] 100 subscribed, 1 next'   │  0.03534182979824568   │  0.03379999939352274  │  0.07440000027418137  │ 0.7415500120208155 │
│   19    │    '[pr] 100 subscribed, 1 next'     │   0.0387508219938843   │  0.0353990001603961   │   0.075399000197649   │ 0.8391257949669215 │
│   20    │ '[master] 100 subscribed, 100 next'  │   0.201847207995411    │  0.19289899989962578  │  0.4039989998564124   │ 0.5170851608490207 │
│   21    │   '[pr] 100 subscribed, 100 next'    │  0.16869005630528555   │  0.1540999999269843   │  0.41039799991995096  │ 0.6719182298208819 │
│   22    │  '[master] 1000 subscribed, 1 next'  │   1.5724196430016308   │   1.404395000077784   │   2.982989000156522   │ 1.3230435037243793 │
│   23    │    '[pr] 1000 subscribed, 1 next'    │  0.38491055010296404   │  0.34489900059998035  │  0.7981970002874732   │ 0.7066891146691681 │
│   24    │ '[master] 1000 subscribed, 100 next' │   3.4955092910996637   │   3.423287999816239   │  12.234055999666452   │ 1.1369695485258002 │
│   25    │   '[pr] 1000 subscribed, 100 next'   │   1.7436433385007084   │  1.9780930001288652   │   3.649686999619007   │ 0.534574044984933  │
└─────────┴──────────────────────────────────────┴────────────────────────┴───────────────────────┴───────────────────────┴────────────────────┘
For previous previous version

It used snapshotedObservers = undefined for dirty instead of relying on a separate flag.

┌─────────┬──────────────────────────────────────┬────────────────────────┬────────────────────────┬───────────────────────┬────────────────────┐
│ (index) │                 Name                 │          Mean          │          P75           │          P99          │        RME         │
├─────────┼──────────────────────────────────────┼────────────────────────┼────────────────────────┼───────────────────────┼────────────────────┤
│    0    │     '[master] Creating Subject'      │ 0.00013438917905651826 │  0.000100000761449337  │ 0.0003000004217028618 │ 1.3808905613853422 │
│    1    │       '[pr] Creating Subject'        │ 0.0001629036470950004  │ 0.00019999966025352478 │ 0.0004000002518296242 │ 1.0150111866666365 │
│    2    │   '[master] 0 subscribed, 1 next'    │ 0.00020070787559535244 │ 0.0002000005915760994  │ 0.0004000002518296242 │ 1.1623744271141512 │
│    3    │     '[pr] 0 subscribed, 1 next'      │ 0.00022203701635951922 │ 0.0002000005915760994  │ 0.0005000000819563866 │ 0.6444068421817747 │
│    4    │  '[master] 0 subscribed, 100 next'   │  0.002925753821364004  │ 0.0028999997302889824  │ 0.004700000397861004  │ 0.6675482695040803 │
│    5    │    '[pr] 0 subscribed, 100 next'     │ 0.0029455148154576153  │  0.00279999990016222   │ 0.005699999630451202  │ 0.7283875789421631 │
│    6    │   '[master] 1 subscribed, 1 next'    │ 0.0008836313455785351  │ 0.0008999994024634361  │ 0.0016999999061226845 │ 2.307640350615159  │
│    7    │     '[pr] 1 subscribed, 1 next'      │ 0.0007251301898595778  │  0.000700000673532486  │ 0.0014000004157423973 │ 0.5369440230363686 │
│    8    │  '[master] 1 subscribed, 100 next'   │  0.004941166842718795  │ 0.0046000005677342415  │ 0.009399999864399433  │ 0.6682103605826338 │
│    9    │    '[pr] 1 subscribed, 100 next'     │  0.005783039856998958  │  0.006799999624490738  │ 0.010099999606609344  │ 2.760054332032325  │
│   10    │   '[master] 3 subscribed, 1 next'    │ 0.0016769644515204616  │  0.001600000075995922  │ 0.0027000000700354576 │ 1.227012134545868  │
│   11    │     '[pr] 3 subscribed, 1 next'      │ 0.0016556261053327526  │  0.001600000075995922  │ 0.0029999995604157448 │ 0.5915214577550268 │
│   12    │  '[master] 3 subscribed, 100 next'   │  0.008435009342731267  │  0.007799999788403511  │ 0.015899999998509884  │ 0.7330348607559893 │
│   13    │    '[pr] 3 subscribed, 100 next'     │  0.008118471813221768  │  0.007699999958276749  │ 0.015999999828636646  │ 0.6762544367799905 │
│   14    │   '[master] 10 subscribed, 1 next'   │  0.004619361542797714  │  0.00430000014603138   │ 0.008700000122189522  │ 0.6865332120986498 │
│   15    │     '[pr] 10 subscribed, 1 next'     │  0.004598094030228484  │  0.004500000737607479  │ 0.007698999717831612  │ 0.6325973043805078 │
│   16    │  '[master] 10 subscribed, 100 next'  │  0.022130750062305478  │  0.018799999728798866  │  0.04339999984949827  │ 2.150985307759586  │
│   17    │    '[pr] 10 subscribed, 100 next'    │  0.019448945272817215  │  0.018399999476969242  │  0.03890000004321337  │ 0.7046440921562903 │
│   18    │  '[master] 100 subscribed, 1 next'   │  0.03653645173544088   │  0.034199999645352364  │  0.07739999983459711  │ 0.8187488838632786 │
│   19    │    '[pr] 100 subscribed, 1 next'     │  0.036315523351770795  │  0.034500000067055225  │  0.07210000045597553  │ 0.7989402616875521 │
│   20    │ '[master] 100 subscribed, 100 next'  │   0.2089092620057985   │  0.19649900030344725   │  0.4164990000426769   │ 0.5958587469091308 │
│   21    │   '[pr] 100 subscribed, 100 next'    │   0.1770285663041286   │  0.15689999982714653   │  0.43609900027513504  │ 0.9649566330973336 │
│   22    │  '[master] 1000 subscribed, 1 next'  │   1.5781902391046285   │   1.4167949995025992   │  3.3108860002830625   │ 0.9724061547552825 │
│   23    │    '[pr] 1000 subscribed, 1 next'    │  0.39690413890369236   │  0.35139899980276823   │  0.8345969999209046   │ 0.7682173831587366 │
│   24    │ '[master] 1000 subscribed, 100 next' │   3.2799664519059473   │   3.4474869994446635   │   6.460476000793278   │ 0.4891545002124059 │
│   25    │   '[pr] 1000 subscribed, 100 next'   │   1.9804286305020564   │   2.026992999948561    │   6.734376000240445   │ 1.2118338955923968 │
└─────────┴──────────────────────────────────────┴────────────────────────┴────────────────────────┴───────────────────────┴────────────────────┘

All these measurements have been done on a codespace of GitHub against 10k runs. I'll try to issue one against even more runs.

@dubzzz
Copy link
Contributor Author

dubzzz commented Mar 30, 2023

Same benchmark but with 1,000,000 iterations:

┌─────────┬─────────────────────────────────────┬────────────────────────┬────────────────────────┬────────────────────────┬─────────────────────┐
│ (index) │                Name                 │          Mean          │          P75           │          P99           │         RME         │
├─────────┼─────────────────────────────────────┼────────────────────────┼────────────────────────┼────────────────────────┼─────────────────────┤
│    0    │     '[master] Creating Subject'     │ 0.00013759328511377957 │ 0.00019999966025352478 │ 0.00039999932050704956 │ 0.33501543849907456 │
│    1    │       '[pr] Creating Subject'       │ 0.00022728897428750432 │ 0.00029999949038028717 │ 0.0005000000819563866  │  4.775231779343227  │
│    2    │   '[master] 0 subscribed, 1 next'   │  0.000341275965122363  │ 0.0004000002518296242  │  0.000599999912083149  │  1.114983178579931  │
│    3    │     '[pr] 0 subscribed, 1 next'     │ 0.00019064141631205663 │ 0.0002000005915760994  │ 0.0005000000819563866  │  4.427243915247421  │
│    4    │  '[master] 0 subscribed, 100 next'  │  0.003445461113855243  │ 0.0032989997416734695  │  0.006000000052154064  │ 1.8685515250963507  │
│    5    │    '[pr] 0 subscribed, 100 next'    │ 0.00044625133478185317 │  0.000499999150633812  │ 0.0009000003337860107  │ 0.6086487209661724  │
│    6    │   '[master] 1 subscribed, 1 next'   │ 0.0008947599909277633  │ 0.0010000001639127731  │ 0.0017999997362494469  │ 0.8469281728715339  │
│    7    │     '[pr] 1 subscribed, 1 next'     │ 0.0007666839421559125  │ 0.0007999995723366737  │ 0.0015000002458691597  │  0.462126717617466  │
│    8    │  '[master] 1 subscribed, 100 next'  │  0.004968361307883635  │  0.004700000397861004  │  0.009399999864399433  │ 0.3859075153627072  │
│    9    │    '[pr] 1 subscribed, 100 next'    │ 0.0013845615506991745  │  0.001300000585615635  │ 0.0027000000700354576  │ 0.8215533645812437  │
│   10    │   '[master] 3 subscribed, 1 next'   │ 0.0018689319399669765  │ 0.0017999997362494469  │ 0.0034999996423721313  │ 0.7049844808800837  │
│   11    │     '[pr] 3 subscribed, 1 next'     │ 0.0016349651360698045  │  0.001600000075995922  │  0.003100000321865082  │ 0.3527873535513193  │
│   12    │  '[master] 3 subscribed, 100 next'  │  0.008423075634939596  │  0.007800000719726086  │  0.01619999948889017   │ 0.29118011818976947 │
│   13    │    '[pr] 3 subscribed, 100 next'    │ 0.0030171170289674774  │  0.002900000661611557  │  0.005400000140070915  │ 0.47993844836563104 │
│   14    │  '[master] 10 subscribed, 1 next'   │  0.004465225901249796  │  0.004200000315904617  │  0.008200000040233135  │ 0.5787421444467985  │
│   15    │    '[pr] 10 subscribed, 1 next'     │  0.004932518482254818  │  0.004800000227987766  │  0.008999999612569809  │ 0.19471452904506512 │
│   16    │ '[master] 10 subscribed, 100 next'  │  0.02027628469098825   │  0.018200000748038292  │  0.040300000458955765  │ 0.5120671472684313  │
│   17    │   '[pr] 10 subscribed, 100 next'    │  0.008474460474032908  │  0.007999999448657036  │  0.01510000042617321   │ 0.2584698024940223  │
│   18    │  '[master] 100 subscribed, 1 next'  │   0.0368288599881772   │  0.03259999956935644   │  0.08150000032037497   │ 0.4215064656065205  │
│   19    │    '[pr] 100 subscribed, 1 next'    │  0.038937690858443265  │  0.03500000014901161   │  0.07730000000447035   │ 0.2287692086159684  │
│   20    │ '[master] 100 subscribed, 100 next' │  0.17048554184314701   │  0.15249900054186583   │  0.37639900017529726   │  0.159418273229607  │
│   21    │   '[pr] 100 subscribed, 100 next'   │  0.07705720384817198   │  0.06880000047385693   │   0.1596990004181862   │ 0.22773070779270208 │
└─────────┴─────────────────────────────────────┴────────────────────────┴────────────────────────┴────────────────────────┴─────────────────────┘

(before (mean) / after / (before divided by after))

  • Creating Subject: 0.00013759328511377957 / 0.00022728897428750432 / 0.6 🔴
  • 0 subscribed, 1 next: 0.000341275965122363 / 0.00019064141631205663 / 1.8 🟢
  • 0 subscribed, 100 next: 0.003445461113855243 / 0.00044625133478185317 / 7.7 🟢
  • 1 subscribed, 1 next: 0.0008947599909277633 / 0.0007666839421559125 / 1.2 🟢
  • 1 subscribed, 100 next: 0.004968361307883635 / 0.0013845615506991745 / 3.6 🟢
  • 3 subscribed, 1 next: 0.0018689319399669765 / 0.0016349651360698045 / 1.1 🟢
  • 3 subscribed, 100 next: 0.008423075634939596 / 0.0030171170289674774 / 2.8 🟢
  • 10 subscribed, 1 next: 0.004465225901249796 / 0.004932518482254818 / 0.9 🟠
  • 10 subscribed, 100 next: 0.02027628469098825 / 0.008474460474032908 / 2.4 🟢
  • 100 subscribed, 1 next: 0.0368288599881772 / 0.038937690858443265 / 0.9 🟠
  • 100 subscribed, 100 next: 0.17048554184314701 / 0.07705720384817198 / 2.2 🟢

@benlesh benlesh self-assigned this Apr 4, 2023
+ Loops over indices of the observers array rather than creating a new iterator and using for-of for each emission
+ Removes unnecessary dirty boolean in favor of clearing the snapshot, which frees up memory a little
+ Centralizes observers list teardown and ensures it's called during `unsubscribe()`.
@dubzzz
Copy link
Contributor Author

dubzzz commented Apr 4, 2023

@benlesh do you want me to re-run the benchmarks following your changes?

@benlesh
Copy link
Member

benlesh commented Apr 4, 2023

@dubzzz I re-ran it, with some alterations. The more common case is that something will next thousands (or even millions) of times. Generally speaking, while subscription is a hot path, next is the "hottest" path in RxJS. The additional commit I added made about a 4x performance difference when dealing with 10,000 nexted values.

┌─────────┬───────────────────────────────────────────────┬────────────────────┬────────────────────┬────────────────────┬─────────────────────┐
│ (index) │                   Name                        │        Mean        │        P75         │        P99         │         RME         │
├─────────┼───────────────────────────────────────────────┼────────────────────┼────────────────────┼────────────────────┼─────────────────────┤
│    0    │ '[old commit] 1,000 subscribed, 10,000 next'  │ 103.60927042180597 │ 104.87890499830246 │ 117.56002202630043 │ 0.07193713572665707 │
│    1    │ '[new commit]  1,000 subscribed, 10,000 next' │ 25.604613090211153 │ 25.85596400499344  │ 28.47809898853302  │ 0.08366443995288589 │
└─────────┴───────────────────────────────────────────────┴────────────────────┴────────────────────┴────────────────────┴─────────────────────┘

┌─────────┬───────────────────────────────────────────┬─────────────────────┬─────────────────────┬─────────────────────┬────────────────────┐
│ (index) │                 Name                      │        Mean         │         P75         │         P99         │        RME         │
├─────────┼───────────────────────────────────────────┼─────────────────────┼─────────────────────┼─────────────────────┼────────────────────┤
│    0    │ '[old commit] 1 subscribed, 10,000 next'  │ 0.31226582655906676 │ 0.3486909866333008  │ 0.6682980060577393  │ 0.9511541628967262 │
│    1    │ '[new commit]  1 subscribed, 10,000 next' │ 0.07090659905672074 │ 0.08109098672866821 │ 0.12429198622703552 │ 0.7590550599162128 │
└─────────┴───────────────────────────────────────────┴─────────────────────┴─────────────────────┴─────────────────────┴────────────────────┘

@benlesh benlesh merged commit dbbae80 into ReactiveX:master Apr 4, 2023
@benlesh
Copy link
Member

benlesh commented Apr 4, 2023

Thank you, @dubzzz

@dubzzz
Copy link
Contributor Author

dubzzz commented Apr 30, 2025

Hi @benlesh,

Just wondering if there is a chance to have this fix included into one stable release soon.
Maybe a back-port to 7.x versions?

Thanks in advance for your time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants