Exploring Jujitsu (jj)

With the news that Firefox development is moving to git, and my own dislike of the git command line interface, I have a few months to figure out if there's a story that doesn't involve the git cli that can get me a comfortable development flow.

There's a few candidates, and each is going to take some time to correctly evaluate. Today, I have started evaluating Jujitsu, a git compatible version control system with some interesting properties.

  • The CLI is very mercurial inspired, and shares a lot of commonalities in supported processes (i.e anonymous head based development)
  • The default log is very similar to mozilla's hg wip
  • It considers the working directory to be a revision, which is an interesting policy.

Here's how I have started evaluating Jujitsu.

  1. First I created a new clone of unified which I called unified-git. Then, using the commands described by glandium in his recent blog post about the history of version control at mozilla I converted that repo to have a git object store in the background.
  2. I then installed Jujitsu. First I did cargo install binstall, then I did cargo cargo-binstall jj to get the binary of jj.
  3. I then made a co-located repository, by initializing jujitsu with the existing git repo: jj init --git-repo=.

After this, I played around, and managed to create a single commit which I have already landed (a comment fix, but still, it was a good exploration of workflow).

There is however, I believe a showstopper bug on my mac, which will prevent me from using jujitsu seriously on my local machine -- I will likely still investigate the potential on my linux build box however.

The bug is this one, and is caused by a poor interaction between jujitsu and insensitive file systems. It means that my working copy will always show changed files existing (at least on a gecko-dev derived repo), which makes a lot of the jujitsu magic and workflow hard.

Some notes from my exploration:

Speed:

This was gently disappointing. While the initial creation of the repo was fast (jj init took 1m11s on my M2 Macbook Pro), every operation by default does a snapshot of the repo state. Likely because of the aforementioned bug, this leads to surprising outcomes: for example, jj log is way slower than hg wip on the same machine (3.8s vs 2s). Of course, if you put jj log --ignore-working-copy, then it's way faster (0.6s), but I don't yet know if that's a usable working system.

Workflow

I was pretty frustrated by this, but in hindsight a lot of the issues came from having the working copy always seeming dirty. This needs more exploration.

  • jj split was quite nice. I was surprised to find out jj histedit doesn't yet exist
  • I couldn't figure out the jj equivalent of hg up . --clean -- this could be every history navigating tool, but because of the bug, it didn't feel like it.

Interaction with Mozilla tools

moz-phab didn't like the fact that my head was detached, and refused to deal with my commit. I had to use git to make a branch (for some reason a Jujitsu branch didn't seem to suffice). Even then, I'm used to moz-phab largely figuring out what commits to submit, but for some reason it really really struggled here. I'm not sure if that's a git problem or a Jujitsu one, but to submit my commit I had to give both ends of a commit range to have it actually do something.

Conclusion

I doubt this will be the last jujitsu post I write -- I'm very interested in trying it in a non-broken state; the fact that it's broken on my Mac however is going to really harm it's ability to become my default.

I've got some other tools I'd like to look into:

  • I've played with Sapling before, but because there's no backing git repo, it may not serve my purposes, as moz-phab wont work (push to try as well, I'll bet) but... maybe if I go the Steve Fink route and write my own phabricator submission tool... maybe it would work.
  • git-branchless looks right up my alley, and is the next took to be evaluated methinks.

Edited: Fixed the cargo-binstall install instruction (previously I said cargo install binstall, but that's an abandoned crate, not the one you want).

Mozilla: 5 years

I missed my 5 year anniversary at Mozilla by a few days here.

As is my tradition, here’s my Bugzilla user stats (from today — I am 3 days late from my real anniversary which was the 27th)

  • Bugs filed 729
  • Comments made 3359
  • Assigned to 365
  • Commented on 1209
  • Patches submitted 1052
  • Patches reviewed 94
  • Bugs poked 1813

The last year was a big one. Tackled my biggest project to date, which ironically, wasn’t even a SpiderMonkey project really: I worked on reimplementing the WHATWG Streams standard inside the DOM. With the help of other Mozillians, we now have the most conformant implementation of the Streams specification by WPT testing. I became the module owner of the resulting DOM Streams module.

I also managed to get a change into the HTML spec, which is a pretty neat outcome.

I’m sure there’s other stuff I did… but I’m procrastinating on something by writing this blog post, and I should get back to that.

A Brief note on Environments and Scopes in SpiderMonkey

To finish telling the story of generators I needed to have a clearer understanding of Environments in SpiderMonkey. Thanks to a discussion with Jason I was able to cobble together some understanding.

JavaScript keeps track of bindings: This is a name corresponding to a value. For example, local and global variables, function arguments, class names -- all of these are bindings. These bindings have some interesting (challenging) properties:

  1. Bindings are nested: This implies two things: First, name lookup proceeds by walking the enclosing bindings looking for a definition of a name. Second, you can shadow a binding by creating a new binding of the same name in an inner scope.
  2. Bindings can be captured: When you create a closure by creating a function, any bindings not defined in the function are captured for when the function is invoked.
  3. Bindings are dynamic: Outside of strict mode, direct eval is capable of adding a new binding: eval('var b = 10'); x = b; works, even though the binding b didn't exist before the eval.

In SpiderMonkey, bindings are implemented with two complementary mechanisms: Scopes and Environments.

The most important distinction to keep in mind is that in SpiderMonkey, Scopes are used to track static binding information. This is information which is always true depending solely on where you are textually in the program, and determined by the parser directly.

Environments are used to track dynamic binding information. As you execute the JS program, the live values of the bindings are kept in environments. As a result, there can be many environments associated with a given scope. Unlike Scopes, Environments are also real JavaScript objects that are just never exposed to the programmer, created as the program executes. Each environment is also linked to its parent environment, the one corresponding to the enclosing scope. As a result of this linking, we often talk about environment chains, referring to not just the single environment, but all enclosing ones too.

Let's work through a specific, albeit very artificial, example to help clarify. In order to avoid the complexity of the real implementation, which has many optimizations and hairy portions, I will instead tell a simplified story which nevertheless attempts to convey the flavour of the issue.

function f(x) {
  let a = 'A: ' + x;
  if (x > 10) {
    let z = a + ' > 10 (z)';
    return function() {
      return z;
    }
  }
  return function() {
    return a + ' <= 10';
  }
}

var f1 = f(0);
var f2 = f(12);

print(f1())  // => A: 0 <= 10
print(f2());  // => A: 12 > 10 (z)

In function f we have two major scopes: There is the scope body for the whole function f, and nested inside is the scope for the body of the if statement.

Recalling that the scopes keep track of the static information, these scopes let us know statically where we must have a binding for a, z, and x. The scopes also know statically where the storage for these bindings is located -- either in stack slots, in some environment object on the environment chain, or in a special object property (in the case of with bindings, or the global object).

When we start executing a call to f(), the first thing that happens is the creation of an environment object to hold the value of a. Since each new environment points to the enclosing one, it will be the end of the environment chain. The JavaScript execution frame is updated so that its 'current environment' points to the newly created environment. Then, if we enter the conditional, the same process is repeated with a new environment to hold z.

In SpiderMonkey, whenever a function is created, it captures the current environment pointer. This is how variable values are captured: in this example, how f1 remembers the values of a and x, and how f2 remembers the values of a, x and z: When we invoke f1 or f2, the new environment created for the function invocation uses the captured environment as the ‘enclosing’ environment, and so the lookup chain has access to the values in the environment of the function creation.

So when we invoke f2, we create an environment for the call. It’s parent environment is the environment where it was created, which contains the binding for z. That environment’s parent is in turn the enclosing parent, which has the environment for a, and its parent has the binding for x.

‍               f   x: 12
                ^
                |
                |
      First F env  a: 'A: 12'
                ^
                |
                |
  Conditional env  z: 'A: 12 > 10 (z)'
                ^
                |
                |
f2 invocation env (empty)

When a binding is on the environment chain, often we statically know where it is relative to the current environment, so SpiderMonkey often refers to environment stored variables via "Environment Coordinates". These are a pair (hops, slot), which indicates how many links on the chain need to be followed (hops) and which slot on that resulting object will have the value.

Optimization

If all locals were stored this way, we'd have to allocate an environment object every time the program enters a scope. The allocation alone would be very slow, and it's also bad for locality. We can do better by analysing binding use to optimize storage.

Where a binding isn't captured by any function, instead we try to store the variable as a slot on the stack frame. However, when a variable is captured, we must store it in the environment so that it can be read on subsequent invocation of the capturing function. In SpiderMonkey the term used for this is "Aliased". So when you see mention of 'an aliased binding', or an opcode like GetAliasedVar, you know the binding is stored on the environment chain.

A direct eval can create a function that captures any or all of the bindings in its environment. So, the mere existence of a direct eval causes all bindings in all enclosing non-global scopes to be marked as aliased.

In some situations, we can also elide the creation of an environment, if we know that it will never contain any bindings.

Conclusion

Hopefully this is a helpful sketch of how variable bindings are handled in SpiderMonkey. There’s loads more complexity in this area of SpiderMonkey that I didn’t cover, but I nevertheless tried to cover the basics.

Acknowledgements

Thanks very much to Jason for previewing a draft of this post and making very helpful suggestions! Any errors are mine however.

How do Generators... Generate, in SpiderMonkey?

I'm going to be looking at async functions and generators over the next little while, which requires I understand them. My previous experience has been that writing about things in a structured fashion helps me learn-by-teaching. So this blog post (series?) is me learning about generators by teaching.

We'll start from the basics of the language feature, and then continue into more specifics.

Basics of Generators

Generators are special objects that return a sequence of values when you call their next() method. The sequence of values provided is specified by code, which backs a generator.

To create a JS Generator, you use the function* syntax:

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

This function, when called, instead of running, returns a generator.

var g = gen();

At this point, none of the code backing the generator has run yet. Once you invoke the next method on the generator g, then the body of the function runs until a yield. At the yield point, execution of the function body stops, and the caller of next is returned an object with value and done properties: value is the argument to the yield, and done is false if there is more results to be yielded, and true if the generator is finished. Subsequent calls to next will simply return {value: undefined, done: true}.

g.next();  // {value: 1,          done: false}
g.next();  // {value: 2,          done: false}
g.next();  // {value: 3,          done: false}
g.next();  // {value: undefined,  done: true}
g.next();  // {value: undefined,  done: true}

This protocol is understood by JavaScript features, like for ... of loops:

let res = []
for (v of gen()) { 
  res.push(v);
}
v; // [1,2,3]

When you call .next() it's possible to provide an argument. That becomes the value of the yield expression when the generator is resumed.

Investigating a basic generator:

Let's look at the bytecode for a generator function. With a debug build of SpiderMonkey, you can dump bytecode with the dis function: dis(gen) produces the following fairly substantial chunk of Bytecode

flags: NEEDS_CALLOBJECT
loc     op
-----   --
main:
00000:  Generator                       # GENERATOR
00001:  SetAliasedVar ".generator" (hops = 0, slot = 2) # GENERATOR
00006:  InitialYield 0                  # RVAL GENERATOR RESUMEKIND
00010:  AfterYield (ic: 1)              # RVAL GENERATOR RESUMEKIND
00015:  CheckResumeKind                 # RVAL
00016:  Pop                             # 
00017:  NewObject ({value:(void 0), done:(void 0)}) # OBJ
00022:  One                             # OBJ 1
00023:  InitProp "value"                # OBJ
00028:  False                           # OBJ false
00029:  InitProp "done"                 # OBJ
00034:  GetAliasedVar ".generator" (hops = 0, slot = 2) # OBJ .generator
00039:  Yield 1                         # RVAL GENERATOR RESUMEKIND
00043:  AfterYield (ic: 5)              # RVAL GENERATOR RESUMEKIND
00048:  CheckResumeKind                 # RVAL
00049:  Pop                             # 
00050:  NewObject ({value:(void 0), done:(void 0)}) # OBJ
00055:  Int8 2                          # OBJ 2
00057:  InitProp "value"                # OBJ
00062:  False                           # OBJ false
00063:  InitProp "done"                 # OBJ
00068:  GetAliasedVar ".generator" (hops = 0, slot = 2) # OBJ .generator
00073:  Yield 2                         # RVAL GENERATOR RESUMEKIND
00077:  AfterYield (ic: 9)              # RVAL GENERATOR RESUMEKIND
00082:  CheckResumeKind                 # RVAL
00083:  Pop                             # 
00084:  NewObject ({value:(void 0), done:(void 0)}) # OBJ
00089:  Int8 3                          # OBJ 3
00091:  InitProp "value"                # OBJ
00096:  False                           # OBJ false
00097:  InitProp "done"                 # OBJ
00102:  GetAliasedVar ".generator" (hops = 0, slot = 2) # OBJ .generator
00107:  Yield 3                         # RVAL GENERATOR RESUMEKIND
00111:  AfterYield (ic: 13)             # RVAL GENERATOR RESUMEKIND
00116:  CheckResumeKind                 # RVAL
00117:  Pop                             # 
00118:  NewObject ({value:(void 0), done:(void 0)}) # OBJ
00123:  Undefined                       # OBJ undefined
00124:  InitProp "value"                # OBJ
00129:  True                            # OBJ true
00130:  InitProp "done"                 # OBJ
00135:  SetRval                         # 
00136:  GetAliasedVar ".generator" (hops = 0, slot = 2) # .generator
00141:  FinalYieldRval                  # 
00142:  RetRval                         # !!! UNREACHABLE !!!

We'll go through this piece by piece to try to understand what's going on.

00000:  Generator                       # GENERATOR
00001:  SetAliasedVar ".generator" (hops = 0, slot = 2) # GENERATOR

Reading this, on the left we have the Bytecode Index (0000), the opcode in the middle Generator, and on the right, after the #, we have the statically determined stack contents.

To understand what opcodes do, the best reference is Opcodes.h in the SpiderMonkey sources, as well as the interpreter implementation of the opcode.

These two opcodes together create a Generator object, and create a binding for the generator under the name .generator, for future access. We use .generator as the name because we know it will never conflict with a user defined JS name because there's no valid syntax to create that name.

00006:  InitialYield 0                  # RVAL GENERATOR RESUMEKIND

InitialYield does three main things: First, it makes the Generator object, allocated above by the Generator opcode, the return value. Then, it calls AbstractGeneratorObject::initialSuspend, after which it pops the current frame off the stack (returning to the caller). We'll discuss the suspend operation shortly.

The contract for generators bytecode is that at the time of generator resumption the stack will be updated to have on it:

  1. The 'result' value of the generator (ie, y, in y = yield x;). This is injected as the argument to .next(...).
  2. The generator object
  3. The resume kind: this indicates if the generator was resumed with .next(), .throw or .return.
00010:  AfterYield (ic: 1)              # RVAL GENERATOR RESUMEKIND
00015:  CheckResumeKind                 # RVAL
00016:  Pop                             #

AfterYield is a book-keeping operation, which we will skip for now.

CheckResumeKind reads the generator resume kind and either: 1) Continues to the next instruction, if the resume kind is next, 2) Throws an exception if the resume kind is either throw or return. The return value is what is on the stack afterwards. Because this was an 'initial yield', there's nothing to consume this return value, so we simply pop it off the stack.

00017:  NewObject ({value:(void 0), done:(void 0)}) # OBJ
00022:  One                             # OBJ 1
00023:  InitProp "value"                # OBJ
00028:  False                           # OBJ false
00029:  InitProp "done"                 # OBJ
00034:  GetAliasedVar ".generator" (hops = 0, slot = 2) # OBJ .generator
00039:  Yield 1                         # RVAL GENERATOR RESUMEKIND

Now that we've gotten through the code executed before the first invocation of next(), we can see the code executed on our first call to next():

  • NewObject allocates the return value of the generator.
  • One pushes 1 onto the stack, and InitProp sets the value property on that object to 1.
  • False and InitProp set the done property on the object to false
  • GetAliasedVar` retrieves the generator from the environment, and pushes it onto the stack.
  • Yield suspends the generator, returning its argument to the caller. Following the same contract for InitialYield, when execution resumes after this bytecode, the stack will have the argument to next/throw/return, the generator, and the resume kind on the stack.

Seeing the above pattern, you can probably easily pick out the next three yields, so I will skip those.

00118:  NewObject ({value:(void 0), done:(void 0)}) # OBJ
00123:  Undefined                       # OBJ undefined
00124:  InitProp "value"                # OBJ
00129:  True                            # OBJ true
00130:  InitProp "done"                 # OBJ
00135:  SetRval                         # 
00136:  GetAliasedVar ".generator" (hops = 0, slot = 2) # .generator
00141:  FinalYieldRval                  # 
00142:  RetRval                         # !!! UNREACHABLE !!!

Once we have exhausted the yields, we get to the end of the generator. At this point, we prepare the {value: returnValue, done: true} object. This is stored into the return value slot, then returned via FinalYieldRval, which closes the generator object and then returns.

Suspend

Suspending a generator means saving the state of the stack frame so that it can be restored. The relevant state here is the state of the expression / interpreter stack.

In a slightly more complicated generator we can see this happen:

function *gen2() { 
    return 10 + (yield 1)
}

When we disassemble this (dis(gen2)) we can see the relevant bits here:

00017:  NewObject ({value:(void 0), done:(void 0)}) # OBJ
00022:  Int8 10                         # OBJ 10
00024:  NewObject ({value:(void 0), done:(void 0)}) # OBJ 10 OBJ
00029:  One                             # OBJ 10 OBJ 1
00030:  InitProp "value"                # OBJ 10 OBJ
00035:  False                           # OBJ 10 OBJ false
00036:  InitProp "done"                 # OBJ 10 OBJ
00041:  GetAliasedVar ".generator" (hops = 0, slot = 2) # OBJ 10 OBJ .generator
00046:  Yield 1                         # OBJ 10 RVAL GENERATOR RESUMEKIND
00050:  AfterYield (ic: 6)              # OBJ 10 RVAL GENERATOR RESUMEKIND
00055:  CheckResumeKind                 # OBJ 10 RVAL
00056:  Add                             # OBJ (10 + RVAL)

The first NewObject pushed is the one for the return value; the second is the one returned by the yield. If you look at the yield, you can see that the return object and the literal 10 are still on the stack when we yield. These values need to be saved on suspend.

For that, the method AbstractGeneratorObject::suspend exists. This method has three main responsibilities.

  1. Keeping track of the resume index: this is where the generator should start executing on resumption.
  2. Keeps track of the frame's environment chain. I'll discuss the environment chain in a moment.
  3. Copying the values out of the interpreter stack into an array stored on the generator object (possibly recycling a previously allocated array, extending it if necessary).

As near as I can tell, the major difference between InitialYield and Yield is that InitialYield is made aware that a priori we know we will have nothing on the expression stack to be saved.

The environment chain needs to be maintained by the generator as it will change over the execution of a generator. For example:

function *gen_with_environment(x) { 
  if (x) {
      var y = 10; 
      let z = 12; 
      yield 1; 
      yield z + 1; 
  }
  yield 2;  
}

The binding let z binding is only available inside of the lexical block defined by the conditional braces. This is managed within the engine by creating a new lexical environment object for the block with the braces, which is made the 'current' environment when the braces are entered. As a result, when we yield, we need to know which lexical environment to restore. The same is not true of the var y binding, which would not create a new environment, as the language hoists var bindings to the top of function definitions.

It's worth noting that environments are mutable in JavaScript, as a direct eval is allowed to add new bindings:

function* g() {
  eval('var b = 10;')
  yield b;
}

so we must keep track of the precise environment to be restored, as it may have been mutated.

Resume and .next()

The above essentially covered how generators are created, and how we leave a generator frame. To see how we get back in, we want to look at the implementation of GeneratorNext, which is self-hosted JavaScript that implements Generator.prototype.next. Self-hosted JS is a special dialect of JS implemented in SpiderMonkey with elevated permissions that is written specifically for engine functionality.

function GeneratorNext(val) {
    // The IsSuspendedGenerator call below is not necessary for correctness.
    // It's a performance optimization to check for the common case with a
    // single call. It's also inlined in Baseline.

    if (!IsSuspendedGenerator(this)) {
        if (!IsObject(this) || !IsGeneratorObject(this))
            return callFunction(CallGeneratorMethodIfWrapped, this, val, "GeneratorNext");

        if (GeneratorObjectIsClosed(this))
            return { value: undefined, done: true };

        if (GeneratorIsRunning(this))
            ThrowTypeError(JSMSG_NESTING_GENERATOR);
    }

    try {
        return resumeGenerator(this, val, "next");
    } catch (e) {
        if (!GeneratorObjectIsClosed(this))
            GeneratorSetClosed(this);
        throw e;
    }
}

Each function largely does what you would imagine it to do. However, there are some interesting pieces. For example return resumeGenerator(this, val, "next") gets compiled directly to the Resume bytecode, not a function call.

The Resume bytecode calls AbstractGeneratorResume, which takes the previously saved expression stack, restores it to the machine stack, and sets the program counter to the correct value (as determined by the resume index stored in the generator).

Given the previously discussed Yield protocol, it also pushes the argument to .next(), the generator object, and the resume kind.

More Functionality: return, throw

Generator.prototype has two more methods than just .next(): There's also .return and .throw.

  • gen.return(x) closes the generator, and returns {val: x, done: true}
  • gen.throw(x) enters the generator, and throws x (which may be caught by the body of the generator and handled.

The implementation of these are both in builtin/Generator.js. Both of which use resumeGenerator (JSOp::Resume), just with different types and return values. These are then handled by the CheckResumeKind op as appropriate.

Conclusion

Having started with no knowledge of how Generators work in SpiderMonkey, I was pleasantly surprised to find the high level design mostly made sense to me. Things I didn't cover here that might be worth exploring:

  1. The JIT implementation of generators
  2. Async functions are implemented with generators: How does that work?

Acknowledgements:

Many thanks to Iain Ireland, Steve Fink, and Jeff Walden for proof reading this post. Mistakes are mine, feedback very welcome!

Moving Mercurial Changesets to a New Clone

I’m setting up a new laptop, and I didn’t want to copy a whole mercurial clone from one laptop to another. All I wanted to do was move my not-yet-public changesets from my local repo to a new clone.

Turns out, with revsets and bundle, this isn’t too hard!

(OldRepo) $ hg bundle --base 423bdf7a802b -r 'author(mgaudet) and not public()' MattsChanges.bundle

Now, because the base revision is pretty old (because my eldest non-public change I wanted to bring along has a pretty old base), the bundle is big, relative to the number of changesets it's bringing across.

However, it applies nicely!

(NewRepo) $ hg unbundle ../MattsChanges.bundle 
adding changesets
adding manifests
adding file changes
added 20 changesets with 49 changes to 83824 files (+16 heads)

I got confused briefly when I tried to use hg import; it complained abort: ../MattsChanges.bundle: no diffs found which isn't a great error message, but I figured it out.