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

Add FluentMessage #366

Closed
wants to merge 26 commits into from
Closed

Add FluentMessage #366

wants to merge 26 commits into from

Conversation

stasm
Copy link
Contributor

@stasm stasm commented Apr 29, 2019

Fix #365. Obsoletes #322 and #360 (on which it's based).

*/
get messages() {
return this._messages[Symbol.iterator]();
this._dirty = new WeakSet();
Copy link
Collaborator

Choose a reason for hiding this comment

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

what's that? It doesn't seem to be used anywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's used in createScope, and then in Pattern to detect cyclic references.

format(id, args, errors = []) {
if (!this._messages.has(id)) {
errors.push(`Message not found: "${id}"`);
return undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why undefined rather than null? Is the user expected to always check if the message exists before trying to format it? I thought format("non-existing") -> null is a reasonable API use and not an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to used undefined here and when formatting missing attributes in order to tell missing values from null values apart. Consider:

foo =
    .attr = Attr
  • format("foo")null,
  • format("bar")undefined.

Do you think they should both return null instead?

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's an interesting question. I'm torn. For me the question boils down to - should attempting to format a message that doesn't existing in the bundle be an error (which means, you should always call hasMessage before you call format)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the bindings should always check, yes. They should also use formatValue and formatAttribute, rather than format. format is intended for more imperative approaches. The fact that it returns undefined on missing messages was intended to be consistent with formatAttribute returning undefined on missing attributes.

It also works well semantically, I think. The difference between null and undefined maps nicely to Fluent's notions of value-less messages vs. missing messages. Attempting to format a missing message should result in an error being reported. As a developer, there's little incentive for me to intentionally format missing messages :) So it's likely an error scenario, which I'd like to be able to react to.

bundle: this,
dirty: this._dirty,
// TermReferences are resolved in a new scope.
insideTermReference: false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I struggled with that in Rust because of lifetimes. While the exact problem is very rust specific and doesn't warrant design changes, I kinda like what I ended up with - locale_args in the scope - https://github.com/projectfluent/fluent-rs/blob/master/fluent-bundle/src/resolve.rs#L244

Maybe worth considering here? Not a blocker of course.

Copy link
Contributor Author

@stasm stasm Apr 30, 2019

Choose a reason for hiding this comment

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

Nice! So local_args is both a store of arguments passed to the term and a flag meaning we're resolving a term now? I like it.

@@ -55,7 +56,7 @@ const MAX_PLACEABLES = 100;
/**
* Fluent Resource is a structure storing a map of parsed localization entries.
*/
export default class FluentResource extends Map {
export default class FluentResource extends Set {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Beyond updating the comment just a line above, can we consider Array vs. Set here? We won't likely be looking for duplicate-free set, so why not Array?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, let me test it with make perf.

Copy link
Contributor Author

@stasm stasm Apr 30, 2019

Choose a reason for hiding this comment

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

Sweet, extending Array gives a slight speed-up in addResource. I guess iterating over the elements of an array is faster than of a set. Thanks for the suggestion!

Copy link
Collaborator

Choose a reason for hiding this comment

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

well, Set does more - it has to hash and look for duplicates on insertion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know, but interestingly, there's no noticeable difference on the benchmark which inserts. Instead, it's the one which iterates over the elements which sped up :)

@stasm
Copy link
Contributor Author

stasm commented Apr 30, 2019

Here's the output of make perf compared to master:

parse-runtime/simple:
  mean:   2.46 (+3%)
  stdev:  0.49
  sample: 30
create-runtime/simple:
  mean:   0.44 (-54%)
  stdev:  0.12
  sample: 30
resolve-runtime/simple:
  mean:   1.15 (+35%)
  stdev:  0.53
  sample: 30
parse-runtime/preferences:
  mean:   8.36 (-1%)
  stdev:  1.33
  sample: 30
create-runtime/preferences:
  mean:   0.74 (-43%)
  stdev:  0.15
  sample: 30
resolve-runtime/preferences:
  mean:   9.1 (+11%)
  stdev:  1.57
  sample: 30
parse-runtime/menubar:
  mean:   4.95 (+13%)
  stdev:  0.66
  sample: 30
create-runtime/menubar:
  mean:   0.53 (-46%)
  stdev:  0.17
  sample: 30
resolve-runtime/menubar:
  mean:   2.36 (+15%)
  stdev:  0.51
  sample: 30
parse-runtime/workload-low:
  mean:   4.99 (-5%)
  stdev:  0.78
  sample: 30
create-runtime/workload-low:
  mean:   0.46 (-57%)
  stdev:  0.06
  sample: 30
resolve-runtime/workload-low:
  mean:   1.82 (+10%)
  stdev:  0.34
  sample: 30

I think create-runtime (a benchmark which I added when working on this PR; it tests the bundle creation and addResource) is faster mostly because FluentResources aren't subclasses of Map anymore. I was expecting parsing to go up a bit because it now creates FluentMessage instances inside of the FluentResource, but there aren't any significant changes.

We're paying more for resolving and I think it's the price of the encapsulation: bundle.formatValue calls createScope, then message.resolveValue, which calls Type. Only then do we check if the value is a simple string and return it. It happens later than on master which—because no encapsulation is enforced—checks the type of the message right away inside of bundle.format. However, it's still not bad (up to +35% on simple.ftl) and combined with the speed-ups in addResource, a one-time create-parse-format cycle is generally faster, or at least the same, in this PR and on master. For the aforementioned simple.ftl, it takes 4.18 ms on my laptop on master, and 4.05 ms with this PR.

@zbraniecki
Copy link
Collaborator

It would be nice to have a create-parse-format cycle perf end-to-end test :)

@stasm
Copy link
Contributor Author

stasm commented Jul 11, 2019

We're going to the go with the formatPattern proposal in #380.

@stasm stasm closed this Jul 11, 2019
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.

Store messages as FluentMessage in the runtime
3 participants