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

Optimize mapObj #202

Merged
merged 1 commit into from
Mar 7, 2017
Merged

Optimize mapObj #202

merged 1 commit into from
Mar 7, 2017

Conversation

lencioni
Copy link
Collaborator

@lencioni lencioni commented Mar 4, 2017

I did some profiling of StyleSheet.create and noticed that mapObj was a
good target for optimization.

Methodology

I created an HTML document with the following code in it:

<script type="text/javascript" src="./dist/aphrodite.umd.js"></script>
<!-- setup -->
<script type="text/javascript">
// build up an array of styles objects to run our test on
var styles = [];
for (var i = 0; i < 10000; i += 1) {
  styles.push({
    [`a${Math.random()}`]: {
      [`a${Math.random()}`]: Math.random(),
      [`b${Math.random()}`]: String(Math.random()),
      [`c${Math.random()}`]: String(Math.random()),
    },

    [`b${Math.random()}`]: {
      [`a${Math.random()}`]: Math.random(),
      [`b${Math.random()}`]: String(Math.random()),
      [`c${Math.random()}`]: String(Math.random()),
    },

    [`c${Math.random()}`]: {
      [`a${Math.random()}`]: Math.random(),
      [`b${Math.random()}`]: String(Math.random()),
      [`c${Math.random()}`]: String(Math.random()),
    },
  });
}
</script>

<!-- test -->
<script type="text/javascript">
setTimeout(() => {
  performance.mark('start_run');
  for (var i = 0; i < styles.length; i += 1) {
    // prevent caching optimizations
    eval('');
    performance.mark('start_stylesheet_create');
    aphrodite.StyleSheet.create(styles[i]);
    performance.mark('end_stylesheet_create');
    performance.measure(
      'aphrodite.StyleSheet.create',
      'start_stylesheet_create',
      'end_stylesheet_create'
    );
    performance.clearMarks('start_stylesheet_create', 'end_stylesheet_create');
  }
  performance.mark('end_run');
  performance.measure(`Benchmark ${styles.length}`, 'start_run', 'end_run');
  performance.clearMarks();
});
</script>

Then, looking at the timeline tool in Chrome, I loaded the page a few
times before and after this change. Similarly, I ran a CPU profile
before and after this change through 5 page reloads each.

In this test, the timeline was not very helpful, I think because of the
testing overhead. However, the CPU profile was very clear. Before this
change, normalizing for the callback showing up in a different part of
the profile, mapObj took ~317ms and after this change, it drops to
~211ms. StyleSheet.create drops from ~755ms to ~670ms or roughly 11%
faster. The rest of the time in StyleSheet.create is spent in
murmurhash2_32_gc and hashObject.

cc @ljharb

lencioni added a commit to lencioni/aphrodite that referenced this pull request Mar 4, 2017
In profiling StyleSheet.create, I noticed that much of the time was
spent hashing. So, I found a faster hashing algorithm.

The implementation was taken from:

  https://github.com/darkskyapp/string-hash

According to this StackExchange post, this algorithm doesn't have as
good of randomness, but it has about the same percentage of collisions.
I don't think randomness matters for this application, so I think this
is okay.

Using similar methodology to Khan#202, this appears to make StylSheet.create
~15% faster (~220ms to ~185ms).
lencioni added a commit to lencioni/aphrodite that referenced this pull request Mar 4, 2017
In profiling StyleSheet.create, I noticed that much of the time was
spent hashing. So, I found a faster hashing algorithm.

The implementation was taken from:

  https://github.com/darkskyapp/string-hash

According to this StackExchange post, this algorithm doesn't have as
good of randomness, but it has about the same percentage of collisions.
I don't think randomness matters for this application, so I think this
is okay.

  http://softwareengineering.stackexchange.com/a/145633

Using similar methodology to Khan#202, this appears to make StylSheet.create
~15% faster (~220ms to ~185ms).
lencioni added a commit to lencioni/aphrodite that referenced this pull request Mar 4, 2017
In profiling StyleSheet.create, I noticed that much of the time was
spent hashing. So, I found a faster hashing algorithm.

The implementation was taken from:

  https://github.com/darkskyapp/string-hash

According to this StackExchange post, this algorithm doesn't have as
good of randomness, but it has about the same percentage of collisions.
I don't think randomness matters for this application, so I think this
is okay.

  http://softwareengineering.stackexchange.com/a/145633

Using similar methodology to Khan#202, this appears to make StylSheet.create
~15% faster (~220ms to ~185ms).
src/util.js Outdated
const mappedObj = {};
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const [newKey, newValue] = fn.call(undefined, [key, obj[key]]);
Copy link

Choose a reason for hiding this comment

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

i'd think that fn([key, obj[key]]) would be much faster than using .call?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought so too, but this seemed to be a bit better, at least in Chrome. I was thinking it might have to do with thisObj being undefined, but maybe I'll take another look at the perf here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

They seem to be about the same. I'll change it to fn() because it is less weird than fn.call.

I thought maybe it would be faster by removing the array here and passing args directly, but that didn't seem to really make a difference.

src/util.js Outdated
const keys = Object.keys(obj);
const mappedObj = {};
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
Copy link

Choose a reason for hiding this comment

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

if you're looking for performance, avoiding block-scoped vars in loops is a good way to do it - ie, repeating the keys[i] in two places probably will give you a slight bump

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, maybe I'll give it a go. I originally avoided it for readability, and I think the performance improvement will be nominal. But maybe it will help with GC.

lencioni added a commit to lencioni/aphrodite that referenced this pull request Mar 4, 2017
In profiling StyleSheet.create, I noticed that much of the time was
spent hashing. So, I found a faster hashing algorithm.

The implementation was taken from:

  https://github.com/darkskyapp/string-hash

According to this StackExchange post, this algorithm doesn't have as
good of randomness, but it has about the same percentage of collisions.
I don't think randomness matters for this application, so I think this
is okay.

  http://softwareengineering.stackexchange.com/a/145633

Using similar methodology to Khan#202, this appears to make StylSheet.create
~15% faster (~220ms to ~185ms).
@lencioni
Copy link
Collaborator Author

lencioni commented Mar 5, 2017

JSPerf: https://jsperf.com/aphrodite-mapobj-refactor/1

I'm seeing the new version about 10% faster in Chrome, 13% faster in Firefox.

Copy link
Contributor

@xymostech xymostech left a comment

Choose a reason for hiding this comment

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

LGTM! Some small nit. Thanks for doing such thorough performance testing!

src/util.js Outdated
const [newKey, newValue] = fn([keys[i], obj[keys[i]]]);
mappedObj[newKey] = newValue;
}
return mappedObj;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we use 4 space indents

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed!

I did some profiling of StyleSheet.create and noticed that mapObj was a
good target for optimization.

Methodology

I created an HTML document with the following code in it:

```html
<script type="text/javascript" src="./dist/aphrodite.umd.js"></script>
<!-- setup -->
<script type="text/javascript">
// build up an array of styles objects to run our test on
var styles = [];
for (var i = 0; i < 10000; i += 1) {
  styles.push({
    [`a${Math.random()}`]: {
      [`a${Math.random()}`]: Math.random(),
      [`b${Math.random()}`]: String(Math.random()),
      [`c${Math.random()}`]: String(Math.random()),
    },

    [`b${Math.random()}`]: {
      [`a${Math.random()}`]: Math.random(),
      [`b${Math.random()}`]: String(Math.random()),
      [`c${Math.random()}`]: String(Math.random()),
    },

    [`c${Math.random()}`]: {
      [`a${Math.random()}`]: Math.random(),
      [`b${Math.random()}`]: String(Math.random()),
      [`c${Math.random()}`]: String(Math.random()),
    },
  });
}
</script>

<!-- test -->
<script type="text/javascript">
setTimeout(() => {
  performance.mark('start_run');
  for (var i = 0; i < styles.length; i += 1) {
    // prevent caching optimizations
    eval('');
    performance.mark('start_stylesheet_create');
    aphrodite.StyleSheet.create(styles[i]);
    performance.mark('end_stylesheet_create');
    performance.measure(
      'aphrodite.StyleSheet.create',
      'start_stylesheet_create',
      'end_stylesheet_create'
    );
    performance.clearMarks('start_stylesheet_create', 'end_stylesheet_create');
  }
  performance.mark('end_run');
  performance.measure(`Benchmark ${styles.length}`, 'start_run', 'end_run');
  performance.clearMarks();
});
</script>
```

Then, looking at the timeline tool in Chrome, I loaded the page a few
times before and after this change. Similarly, I ran a CPU profile
before and after this change through 5 page reloads each.

In this test, the timeline was not very helpful, I think because of the
testing overhead. However, the CPU profile was very clear. Before this
change, normalizing for the callback showing up in a different part of
the profile, `mapObj` took ~317ms and after this change, it drops to
~211ms. `StyleSheet.create` drops from ~755ms to ~670ms or roughly 11%
faster. The rest of the time in `StyleSheet.create` is spent in
`murmurhash2_32_gc` and `hashObject`.
Copy link
Contributor

@xymostech xymostech left a comment

Choose a reason for hiding this comment

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

LGTM now!

@xymostech xymostech merged commit 5e9a0cb into Khan:master Mar 7, 2017
lencioni added a commit to lencioni/aphrodite that referenced this pull request Mar 7, 2017
In profiling StyleSheet.create, I noticed that much of the time was
spent hashing. So, I found a faster hashing algorithm.

The implementation was taken from:

  https://github.com/darkskyapp/string-hash

According to this StackExchange post, this algorithm doesn't have as
good of randomness, but it has about the same percentage of collisions.
I don't think randomness matters for this application, so I think this
is okay.

  http://softwareengineering.stackexchange.com/a/145633

Using similar methodology to Khan#202, this appears to make StylSheet.create
~15% faster (~220ms to ~185ms).
lencioni added a commit to lencioni/aphrodite that referenced this pull request Mar 8, 2017
In profiling StyleSheet.create, I noticed that much of the time was
spent hashing. So, I found a faster hashing algorithm.

The implementation was taken from:

  https://github.com/darkskyapp/string-hash

According to this StackExchange post, this algorithm doesn't have as
good of randomness, but it has about the same percentage of collisions.
I don't think randomness matters for this application, so I think this
is okay.

  http://softwareengineering.stackexchange.com/a/145633

Using similar methodology to Khan#202, this appears to make StylSheet.create
~15% faster (~220ms to ~185ms).
xymostech pushed a commit that referenced this pull request Mar 8, 2017
* Replace murmur hash with djb2 hash

In profiling StyleSheet.create, I noticed that much of the time was
spent hashing. So, I found a faster hashing algorithm.

The implementation was taken from:

  https://github.com/darkskyapp/string-hash

According to this StackExchange post, this algorithm doesn't have as
good of randomness, but it has about the same percentage of collisions.
I don't think randomness matters for this application, so I think this
is okay.

  http://softwareengineering.stackexchange.com/a/145633

Using similar methodology to #202, this appears to make StylSheet.create
~15% faster (~220ms to ~185ms).

* Depend on string-hash for djb2 hashing algorithm

This is where I copied the code for this algorithm from, seems like we
might as well just bring in the dependency for it.
@lencioni lencioni deleted the mapObj branch July 20, 2017 22:26
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.

3 participants