node-chakracore icon indicating copy to clipboard operation
node-chakracore copied to clipboard

HTTP2 functionality requires changes to work with TTD

Open MSLaguana opened this issue 8 years ago • 16 comments

  • Version: master
  • Platform: all
  • Subsystem: TTD, http2

Some changes to http2 require updates to support TTD properly: the upstream commit is https://github.com/nodejs/node/commit/e46ae99a2a8153d6af84dbf2883f5f0938065a3e and will need similar changes to those in https://github.com/nodejs/node-chakracore/pull/194, or otherwise will require a change in how we handle this scenario.

I spoke with @mrkmarron and he had this to offer:

This aliasing a backing array through a C++ and JS variable just seems like a terrible idea to me and I was hoping that it was a one off optimization. However, it looks like it is spreading so I think we may want to do a bit of work to create an API that we can use to track these cases and which will not create a lot of merge conflicts going forward. E.g. add a few macros for CREATE_ALIASED_BUFFER() and MODIFY_ALIASED_BUFFER() where we can hide the rest of the changes behind them (and maybe even try to upstream as nops).

MSLaguana avatar Aug 17 '17 21:08 MSLaguana

I certainly agree that the aliasing of the backing array is a bit of a hack but it's an effective one in terms of performance. The speedup is absolutely non-trivial. Just as an FYI, I'm also doing this with a key part of the Performance API implementation. Until we have a better solution with a similar or improve performance profile, it's likely to be something we stick with.

jasnell avatar Aug 17 '17 21:08 jasnell

@jasnell thanks for the insight- would Mark's suggestion of creating macros for this pattern be something we could PR upstream?

digitalinfinity avatar Aug 18 '17 00:08 digitalinfinity

Hi James, I am sure the perf improvement is non-trivial. The cost of cross-runtime calls is painful is going to be painful on a hot-paths like this.

In the short term my big concerns are (1) adding TTD and managing merge conflicts and (2) general maintainability. As the code is written now there is no encapsulation or indication that a write is happening to a JS aliased array (and which one it might be). As per my previous comment I think a adding some macros on the C++ side for this idiom would help a lot with these issues and simplify later code changes.

In the longer term I think this is a good opportunity to start looking at what a Node.js embedder API would look like. For example in the case of fs.stat there is a similar native/JS shared array for the fs stats data that is directly written into to avoid the cost of multiple cross runtime boundary calls. But even with this optimization the JS layer still needs to unpack each value in the array and assign to a property in a JS object (line 245 in fs.js). Having an API like: JsCreateFlagObjectFromPropertiesAndValues(size_t propertyCount, JsObject* prototype, ...), called like: JsCreateFlagObjectFromPropertiesAndValues(2, statsProto, "mode", modeVal, "uid", uidVal) would allow all of this to happen in one step in the native code and be great for improved performance, better maintainability, and (for me personally) lower cost/more useful TTD recording.

mrkmarron avatar Aug 18 '17 00:08 mrkmarron

The possibility of adding a macro makes sense. It could get a bit complicated.

jasnell avatar Aug 18 '17 01:08 jasnell

@jasnell I spent some time talking with @mike-kaufman about the macro option. I think we are going to give it a try to see how it seems to work out.

mrkmarron avatar Aug 19 '17 20:08 mrkmarron

Spent some time looking at this yesterday. The macro solution feels pretty kludgy IMO. What about introducing an AliasedBuffer class to allow access to the raw buffer & the JS Array? All writes to the buffer could be routed through an API, which gives a clean place for TTD-tracking code to sit.

Functions should be inlined so perf impact should be minimal. E.g., a class roughly like the following:

    template <class T, class U>
    class AliasedBuffer {
    public:
      AliasedBuffer(const int count);
      ~AliasedBuffer();
      T* GetBuffer();
      v8::Local<U> GetJSArray();

      // set the value at specified index.  Since this is an explicit write, chakra's fork 
      // can add in code to do TTD-specific tracking.  Need to know this is a write 
      // vs a read, so overloading operator[] won't work AFAIU (let me know if not the case)
      void SetValue(const int index, T value);

      // get value at specified index.
      const T GetValue(const int index);

    private:
      T* buffer_;   // raw buffer that is written to
      v8::Global<U> jsArray_;  // JS array over the buffer
    };
  }

So, then a client can have something like

    ...
    private:
       AliasedBuffer* fields_;
      
   public: 
        init() {
            fields_ = new AliasedBuffer<double, Float64Array>(kfieldsCount);
        }
        
       doSoemthing() {
          double status = ...;
          fields_->SetValue(kMyStatusFieldIndex, status);
       }

@jasnell , @mrkmarron , @digitalinfinity - let me know your thoughts on this approach. Particularly

  • any perf concerns?
  • any API concerns w/ what I proposed?
  • Any pushback with merging such a solution upstream?
  • Any gaps in supporting TTD scenarios?

mike-kaufman avatar Aug 22 '17 17:08 mike-kaufman

Ping @addaleax ... would like your input on this also if you don't mind

jasnell avatar Aug 22 '17 17:08 jasnell

This aliasing a backing array through a C++ and JS variable just seems like a terrible idea to me and I was hoping that it was a one off optimization. However, it looks like it is spreading

Fwiw, async-hooks is another place where this also came up.

@mike-kaufman Your proposed solutions sounds good to me!

so overloading operator[] won't work AFAIU (let me know if not the case)

You could make operator[] work by returning a reference-ish object, similar to what std::bitset::operator[] does, but that’s obviously just cosmetics.

addaleax avatar Aug 22 '17 17:08 addaleax

You could make operator[] work by returning a reference-ish object, similar to what std::bitset::operator[] does, but that’s obviously just cosmetics.

Ahh, I see, thanks. My preference is to have separate get/set methods, as it seems simpler, but I'd like to keep things consistent w/ what's in the code base. Let me know if there's anything to suggest one direction or the other.

mike-kaufman avatar Aug 22 '17 17:08 mike-kaufman

I think this works also. The important bit is going to be ensuring that these backing buffers can be set per-Environment and that they are persistent over time. I have no perf concerns with this approach. I agree that an operator[] would be helpful, and by convention we have used operator* consistently in core to provide the equivalent of your GetBuffer(). Also common is using data() or Data() to do the same -- but as @addaleax says, that's just cosmetics.

One other consideration is this: In some cases, we have a single TypedArray backed by a single ArrayBuffer backed by a single backing array. In other cases, we have multiple TypedArray instances backed by a single aggregate ArrayBuffer backed by a single backing array. This is done simply because it is more efficient than having multiple ArrayBuffers. It would be nice if this approach accounted for that model as well.

I believe this is easily something that we could get merged into core and would help make our application of this pattern more consistent. Thank you very much for working on this.

jasnell avatar Aug 22 '17 17:08 jasnell

we have multiple TypedArray instances backed by a single aggregate ArrayBuffer backed by a single backing array

@jasnell - Can you point me to any examples of this? I didn't see it when I was looking around yesterday.

I agree that an operator[] would be helpful, and by convention we have used operator*

OK, makes sense, i'll overload the operators.

mike-kaufman avatar Aug 22 '17 18:08 mike-kaufman

See here: https://github.com/nodejs/node/blob/master/src/node_http2.cc#L1179-L1199

jasnell avatar Aug 22 '17 18:08 jasnell

Just to make sure it doesn’t get lost, operator* should only be const, otherwise it kind of circumvents what your approach is trying to achieve :)

addaleax avatar Aug 22 '17 18:08 addaleax

where's the best place to add unit-tests for such a class?

mike-kaufman avatar Aug 22 '17 18:08 mike-kaufman

Take a look at test/cctest

jasnell avatar Aug 22 '17 20:08 jasnell

@jasnell, @addaleax - thanks for your help. Plz take a look at https://github.com/nodejs/node/pull/15077.

mike-kaufman avatar Aug 29 '17 15:08 mike-kaufman