rawls238 / PlanOut.js

A JavaScript port of Facebook's PlanOut Experimentation Framework

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

planout_core_compatible namespaces are not core compatible

jcwilson opened this issue · comments

Problem

The planout_core_compatible.js distribution in 3.0.* does not allocate namespace segments in a compatible manner.

I have done a little digging into the webpack'd distribution, but I'm at a loss to explain the root cause. The best I can tell is that when SampleBuilder.sample() calls this.compatSampleIndexCalculation() it gets routed to SampleBuilder.compatSampleIndexCalculation which is the non-compatible version, rather than to SampleCoreCompatible.compatSampleIndexCalculation.

As far as I can tell, the compatible random ops are working as expected when called directly. Perhaps it's a bug in webpack?

Reproduction Case

var planout_202 = require('./node_modules/planout/dist/planout_202.js')
var planout = require('./node_modules/planout/dist/planout.js')
var planout_compat = require('./node_modules/planout/dist/planout_core_compatible.js')


Object.getOwnPropertyDescriptors = function getOwnPropertyDescriptors(obj) {
  var descriptors = {};
  for (var prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      descriptors[prop] = Object.getOwnPropertyDescriptor(obj, prop);
    }
  }
  return descriptors;
};

Function.prototype.extend = function extend(proto) {
    var superclass = this;
    var constructor;

    if (!proto.hasOwnProperty('constructor')) {
      Object.defineProperty(proto, 'constructor', {
        value: function () {
            // Default call to superclass as in maxmin classes
            superclass.apply(this, arguments);
        },
        writable: true,
        configurable: true,
        enumerable: false
      });
    }
    constructor = proto.constructor;

    constructor.prototype = Object.create(this.prototype, Object.getOwnPropertyDescriptors(proto));

    return constructor;
};

var range = function(max) {
  var l = [];
  for (var i = 0; i < max; i++) {
    l.push(i);
  }
  return l;
};

function getSample(planout) {
    var assignment = new planout.Assignment('user');
    assignment.set('sampled_segments', new planout.Ops.Random.Sample({
        'choices': range(10),
        'draws': 5,
        'unit': 'user'
    }));
    return assignment.get('sampled_segments');
}

function allocateSegments(planout) {
  var TestNamespace = planout.Namespace.SimpleNamespace.extend({
    setup: function() {
      this.name = 'TestNamespace';
      this.numSegments = 10;
      this.setPrimaryUnit('userId');
    },

    setupExperiments: function() {
      this.addExperiment('My Experiment', planout.Experiment, 5);
    }
  });
  return Object.keys(new TestNamespace().segmentAllocations);
}

function runExperiment(planout) {
  var TestExperiment = planout.Experiment.extend({
    configureLogger: function() {},
    log: function(event) {},
    previouslyLogged: function() { return false; },
    setup: function() {},
    getParamNames: function() { return this.getDefaultParamNames(); },
    assign: function(params, args) {
      params.set('foo', new planout.Ops.Random.UniformChoice({choices: ['a', 'b'], 'unit': args.userId}));
    }
  });

  var result = [];
  for (var i = 0; i < 10; i++) {
    result.push(new TestExperiment({userId: i}).get('foo'));
  }
  return result;
}

function runTests(desc, test) {
  console.log('Testing ' + desc);
  console.log('------------------------');
  planout_202.ExperimentSetup.toggleCompatibleHash(false);
  console.log('202|compat=false: ' + test(planout_202));
  console.log('303|            : ' + test(planout));
  planout_202.ExperimentSetup.toggleCompatibleHash(true);
  console.log('202|compat=true : ' + test(planout_202));
  console.log('303|compatible  : ' + test(planout_compat));
  console.log()
}

runTests('sample functions', getSample);
runTests('experiment assignment', runExperiment);
runTests('namespace segment allocation', allocateSegments);

Output

Testing sample functions
------------------------
202|compat=false: 4,7,5,6,3
303|            : 4,7,5,6,3
202|compat=true : 2,3,9,1,8
303|compatible  : 2,3,9,1,8

Testing experiment assignment
------------------------
202|compat=false: a,a,b,b,b,b,b,b,b,a
303|            : a,a,b,b,b,b,b,b,b,a
202|compat=true : a,a,a,a,a,a,a,a,b,a
303|compatible  : a,a,a,a,a,a,a,a,b,a

Testing namespace segment allocation
------------------------
202|compat=false: 0,1,2,7,9
303|            : 0,1,2,7,9
202|compat=true : 0,1,3,4,6
303|compatible  : 0,1,2,7,9

Notice the last two lines. They should match, but do not.

I considered that it might be due to loading all three versions into one script, but the results are consistent if I just run one version's tests at a time.

This is incredibly interesting and bizarre. I won't be able to look into it until the end of next week at the earliest though, not sure if @mattrheault has some time to take a look.

I don't think the webpack version has been updated in a while so perhaps the easiest first thing to try is seeing if this still reproducible moving to the newest version of webpack

I can't rule out that it's something specific to my setup, somehow. Unfortunately, I just have my one data point.

For reference, though:
node version: 6.10.0
npm version: 3.10.10

I think I can explain what's happening, though I don't have a good answer for fixing it yet.

First, upgrading webpack didn't resolve it, but I don't think it hurt anything.

I noticed that in the Namespace module, we have the following import:

import { Sample, RandomInteger } from "./ops/random.js";

This will essentially "freeze" the non-core-compatible code into the Namespace module. In fact, this pattern is repeated in several other places, even ops/utils.js. When I first encountered this behavior I was confused because my experiments seemed to be operating in core-compatibility mode. But now this explains it - the random part is instantiated and passed in as part of the experiment definition (in assign()). And as I noted above, we get the compatible code if we reference planout.Ops.Random directly.

assign: function(params, args) {
  params.set('foo', new planout.Ops.Random.UniformChoice({choices: ['a', 'b'], 'unit': args.userId}));
}

I have updated the repro code above to reflect these new learnings.

EDIT: Upon closer inspection, the Experiment and Assignment modules are largely unaffected by this anyway, as the only reason they need random is to get PlanOutOpRandom to do some type checking against it - and the core-compatible ops inherit from it anyway. Still, the analysis above still stands up.

Hope this helps.