<commit_before><commit_msg>docs(notebooks): javascript notebook on JSON.parse<commit_after>{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "var Immutable = require('immutable')\n",
    "var _ = require('lodash')\n",
    "\nvar commutable = require('commutable')"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "# Revival\n",
    "\n",
    "JSON.parse takes an extra argument called a reviver:\n",
    "\n",
    "```\n",
    "JSON.parse(text[, reviver])\n",
    "```\n",
    "\n",
    "The reviver accepts two parameters, `key` and `value` and returns the intended `value`. The key will either be a text key on Objects or numbers for when the value is in an Array.\n",
    "\nLet's walk through some sample code to check this out."
   ],
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "// Classic JSON.parse\n",
    "JSON.parse('{\"a\": 2, \"b\": { \"name\": \"dave\" }}')"
   ],
   "outputs": [
    {
     "execution_count": 2,
     "data": {
      "text/plain": [
       "{ a: 2, b: { name: 'dave' } }"
      ]
     },
     "metadata": {},
     "output_type": "execute_result"
    }
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "function reviver(key, value) {\n",
    "    if(key === 'name') {\n",
    "        return value + \" senior\";\n",
    "    }\n",
    "    return value\n",
    "}\n",
    "\nJSON.parse('{\"a\": 2, \"b\": { \"name\": \"dave\" }}', reviver)"
   ],
   "outputs": [
    {
     "execution_count": 3,
     "data": {
      "text/plain": [
       "{ a: 2, b: { name: '<NAME>' } }"
      ]
     },
     "metadata": {},
     "output_type": "execute_result"
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "outputExpanded": false
   },
   "source": [
    "This means you can use this to change values based on a key, though you won't know the nested path of the overall JSON object. \n",
    "\nSince the string is (expected to be) JSON, there are only two types which are not immutable: `Array` and `Object`. You can use this to your advantage to create frozen or Immutable.js objects while parsing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "JSON.parse('{\"a\": 2, \"b\": { \"name\": \"dave\" }}', (k, v) => Object.freeze(v))"
   ],
   "outputs": [
    {
     "execution_count": 4,
     "data": {
      "text/plain": [
       "{ a: 2, b: { name: 'dave' } }"
      ]
     },
     "metadata": {},
     "output_type": "execute_result"
    }
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "function immutableReviver(key, value) {\n",
    "    if (Array.isArray(value)) {\n",
    "        return Immutable.List(value);\n",
    "    }\n",
    "\n",
    "    if (typeof value === 'object') {\n",
    "        return Immutable.Map(value)\n",
    "    }\n",
    "    return value;\n",
    "}"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "Since it seemed handy enough, I put [`immutable-reviver`](https://github.com/rgbkrk/immutable-reviver) on npm. We'll just use the version written here for now though."
   ],
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "revived = JSON.parse('{\"a\": 2, \"b\": { \"name\": \"dave\" }}', immutableReviver)"
   ],
   "outputs": [
    {
     "execution_count": 6,
     "data": {
      "text/plain": [
       "Map { \"a\": 2, \"b\": Map { \"name\": \"dave\" } }"
      ]
     },
     "metadata": {},
     "output_type": "execute_result"
    }
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "revived.getIn(['b', 'name'])"
   ],
   "outputs": [
    {
     "execution_count": 7,
     "data": {
      "text/plain": [
       "'dave'"
      ]
     },
     "metadata": {},
     "output_type": "execute_result"
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "outputExpanded": false
   },
   "source": [
    "The reason I started looking into this was because I was trying to see if I could optimize loading of notebooks in nteract. We currently rely on a strategy that goes like:\n",
    "\n",
    "```\n",
    "notebook = JSON.parse(rawNotebook)\n",
    "immutableNotebook = Immutable.fromJS(notebook)\n",
    "\n",
    "ourNotebook = immutableNotebook.map(...).map(...)... // A series of transformations to create our in-memory representation\n",
    "```\n",
    "\n",
    "These transformations are mostly to turn notebook cells from this:\n",
    "\n\n",
    "```\n",
    "{\n",
    "  \"metadata\": {\n",
    "    \"collapsed\": false,\n",
    "    \"outputExpanded\": false\n",
    "  },\n",
    "  \"cell_type\": \"markdown\",\n",
    "  \"source\": [\n",
    "    \"# Outputs you can update by name\\n\",\n",
    "    \"\\n\",\n",
    "    \"This notebook demonstrates the new name-based display functionality in the notebook. Previously, notebooks could only attach output to the cell that was currently being executed:\\n\",\n",
    "    \"\\n\"\n",
    "  ]\n",
    "}\n",
    "```\n",
    "\n",
    "into:\n",
    "\n",
    "```\n",
    "{\n",
    "  \"metadata\": {\n",
    "    \"collapsed\": false,\n",
    "    \"outputExpanded\": false\n",
    "  },\n",
    "  \"cell_type\": \"markdown\",\n",
    "  \"source\": \"# Outputs you can update by name\\n\\nThis notebook demonstrates the new name-based display functionality in the notebook. Previously, notebooks could only attach output to the cell that was currently being executed:\\n\\n\"\n",
    "}\n",
    "```\n",
    "\nThis multi-line string format, introduced by Jupyter, is to accomodate diffing of notebooks in tools like git and GitHub. It's applied to source on cells as well as some output types."
   ]
  },
  {
   "cell_type": "markdown",
   "source": [
    "We can set up a reviver that handles all the keys that are most likely to have [multi-line strings](https://github.com/jupyter/nbformat/blob/62d6eb8803616d198eaa2024604d1fe923f2a7b3/nbformat/v4/nbformat.v4.schema.json#L386). We'll start with those that are media types that we know end up being encoded as an array of strings."
   ],
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "var multilineStringMimetypes = new Set([\n",
    "    'application/javascript',\n",
    "    'text/html',\n",
    "    'text/markdown',\n",
    "    'text/latex',\n",
    "    'image/svg+xml',\n",
    "    'image/gif',\n",
    "    'image/png',\n",
    "    'image/jpeg',\n",
    "    'application/pdf',\n",
    "    'text/plain',\n",
    "]);\n",
    "\n",
    "function immutableNBReviver(key, value) {\n",
    "    if (Array.isArray(value)) {\n",
    "        if(multilineStringMimetypes.has(key)) {\n",
    "            return value.join('')\n",
    "        }\n",
    "        return Immutable.List(value);\n",
    "    }\n",
    "\n",
    "    if (typeof value === 'object') {\n",
    "        return Immutable.Map(value)\n",
    "    }\n",
    "    return value;\n",
    "}"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "We can also set up a \"greedy\" reviver that will also convert `source` and `text` fields. The primary problem with this though, because of how JSON.parse works is that we have no idea if it's a key in a cell where we expect, part of someone else's JSON payload, or in metadata."
   ],
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "var specialKeys = new Set([\n",
    "    'application/javascript',\n",
    "    'text/html',\n",
    "    'text/markdown',\n",
    "    'text/latex',\n",
    "    'image/svg+xml',\n",
    "    'image/gif',\n",
    "    'image/png',\n",
    "    'image/jpeg',\n",
    "    'application/pdf',\n",
    "    'text/plain',\n",
    "    'source',\n",
    "    'text',\n",
    "]);\n",
    "\n",
    "function immutableGreedyReviver(key, value) {\n",
    "    if (Array.isArray(value)) {\n",
    "        if(specialKeys.has(key)) {\n",
    "            return value.join('')\n",
    "        }\n",
    "        return Immutable.List(value);\n",
    "    }\n",
    "\n",
    "    if (typeof value === 'object') {\n",
    "        return Immutable.Map(value)\n",
    "    }\n",
    "    return value;\n",
    "}"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "# Our runtime harnesses\n",
    "\nTo evaluate the speed at which we can revive our objects, we'll set up a little testing harness."
   ],
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "// Some logger that uses process.hrtime that I ripped off Stack Overflow, since we want to use timing in a way that we can't with console.time\n",
    "\n",
    "[ a, o, ms, s, log ] = ( function * () {\n",
    "    yield * [\n",
    "        ( process.hrtime )(),\n",
    "        process.hrtime,\n",
    "        ms => ( ( ms[ 0 ] * 1e9 + ms[ 1 ] ) / 1000000 ),\n",
    "        s  => s / 1000,\n",
    "        () => {\n",
    "            const f = o( a ), msf = ms( f ), sf = s( msf );\n",
    "            return { a, o: f, ms: msf, s: sf };\n",
    "        }\n",
    "    ];\n",
    "} )();"
   ],
   "outputs": [
    {
     "execution_count": 10,
     "data": {
      "text/plain": [
       "{}"
      ]
     },
     "metadata": {},
     "output_type": "execute_result"
    }
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "// Calculate the milliseconds it takes to run f\n",
    "function measure(f) {\n",
    "  start = log()\n",
    "  f()\n",
    "  end = log()\n",
    "  return end.ms - start.ms  \n",
    "}\n",
    "\n",
    "// measure the function run n times, return the mean\n",
    "function runTrials(f, n=1000) {\n",
    "    values = []\n",
    "    for(var ii=0; ii < n; ii++) {\n",
    "        values.push(measure(f))\n",
    "    }\n",
    "    return values.reduce((a, b) => a + b, 0)/n\n",
    "}"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "outputExpanded": false
   },
   "source": [
    "With our harness all set up, we can run through all the notebooks we have locally to see how they perform with different revivers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "notebooks = require('glob').sync('./*.ipynb')\n",
    "\n",
    "for(var notebookPath of notebooks) {\n",
    "    console.log(\"\\n ----- \", path.basename(notebookPath))\n",
    "    raw = fs.readFileSync(notebookPath)\n",
    "    \n",
    "    var tests = [\n",
    "        { name: 'straight JSON.parse', f: () => { JSON.parse(raw) } },\n",
    "        { name: 'Object.freeze', f: () => { JSON.parse(raw, (k, v) => Object.freeze(v)) } },\n",
    "        { name: 'basic Immutable', f: () => { JSON.parse(raw, immutableReviver) } },\n",
    "        { name: 'immutable notebook', f: () => { JSON.parse(raw, immutableNBReviver) } },\n",
    "        { name: 'immutable greedy nb', f: () => { JSON.parse(raw, immutableGreedyReviver) } },\n",
    "        // { name: 'fromJS', f: () => { JSON.parse(raw, (k, v) => Immutable.fromJS(v)) } },\n",
    "        // { name: 'current commutable way', f: () => { commutable.fromJS(JSON.parse(raw)) } },\n",
    "    ]\n",
    "    \n",
    "    for(var test of tests) {\n",
    "        mean = runTrials(test.f, 100)\n",
    "        console.log(_.padEnd(test.name, 30), mean)\n",
    "    }\n",
    "    \n",
    "\n",
    "}\n",
    "\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "text": [
      "\n",
      " -----  altair.ipynb\n",
      "straight JSON.parse            1.0749021599999902\n",
      "Object.freeze                  2.260570740000003\n",
      "basic Immutable                7.50018614\n",
      "immutable notebook             7.798826189999991\n",
      "immutable greedy nb            7.281795899999938\n",
      "\n",
      " -----  display-updates.ipynb\n",
      "straight JSON.parse            0.05507465000002412\n",
      "Object.freeze                  0.2737798099999691\n",
      "basic Immutable                0.4565474500000255\n",
      "immutable notebook             0.39348404999997\n",
      "immutable greedy nb            0.35731991000001473\n",
      "\n",
      " -----  download-stats.ipynb\n",
      "straight JSON.parse            0.03955825000002733\n",
      "Object.freeze                  0.12198090999999295\n",
      "basic Immutable                0.15957000999999763\n",
      "immutable notebook             0.14660479000001034\n",
      "immutable greedy nb            0.1560780199999499\n",
      "\n",
      " -----  geojson.ipynb\n",
      "straight JSON.parse            0.05292227999999341\n",
      "Object.freeze                  0.14175530000001346\n",
      "basic Immutable                0.24063294999996288\n",
      "immutable notebook             0.24815407999999478\n",
      "immutable greedy nb            0.37828451999995194\n",
      "\n",
      " -----  immutable-revival.ipynb\n",
      "straight JSON.parse            0.11685697000005348\n",
      "Object.freeze                  0.3373106900000039\n",
      "basic Immutable                0.5352781999999934\n",
      "immutable notebook             0.4722430500000246\n",
      "immutable greedy nb            0.5092820300000312\n",
      "\n",
      " -----  intro.ipynb\n",
      "straight JSON.parse            0.03360039999997753\n",
      "Object.freeze                  0.08881936000006135\n",
      "basic Immutable                0.1650313499999993\n",
      "immutable notebook             0.15445357000009607\n",
      "immutable greedy nb            0.15080755999995746\n",
      "\n",
      " -----  pandas-to-geojson.ipynb\n",
      "straight JSON.parse            0.0994644199999948\n",
      "Object.freeze                  0.23873196000007738\n",
      "basic Immutable                0.40407584000001406\n",
      "immutable notebook             0.4069329999999627\n",
      "immutable greedy nb            0.4162412399999266\n",
      "\n",
      " -----  plotly.ipynb\n",
      "straight JSON.parse            0.8092693299999882\n",
      "Object.freeze                  2.0506729500000347\n",
      "basic Immutable                2.3805847400000038\n",
      "immutable notebook             2.8385263100000158\n",
      "immutable greedy nb            2.562465860000002\n",
      "\n",
      " -----  plotlyr.ipynb\n",
      "straight JSON.parse            0.01012205000006361\n",
      "Object.freeze                  0.03471055000004526\n",
      "basic Immutable                0.052657159999989746\n",
      "immutable notebook             0.11424691999999595\n",
      "immutable greedy nb            0.05146689999996852\n"
     ],
     "output_type": "stream"
    }
   ]
  },
  {
   "cell_type": "markdown",
   "source": [
    "# Evaluating revivers for notebook loading.\n",
    "\nWithin nteract we are inevitably going to end up creating an immutable structure. These measurements only make sense in the context of running both the initial `JSON.parse` followed by the transformations. To give it a rough guess, I'll only compare a few I can evaluate."
   ],
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [
    "notebooks = require('glob').sync('./*.ipynb')\n",
    "\n",
    "for(var notebookPath of notebooks) {\n",
    "    console.log(\"\\n ----- \", path.basename(notebookPath))\n",
    "    raw = fs.readFileSync(notebookPath)\n",
    "    \n",
    "    var tests = [\n",
    "        { name: 'straight JSON.parse baseline', f: () => { JSON.parse(raw) } },\n",
    "        { name: 'Object.freeze baseline', f: () => { JSON.parse(raw, (k,v) => Object.freeze(v)) } },\n",
    "        { name: 'straight JSON.parse then commutable conversion', f: () => { commutable.fromJS(JSON.parse(raw)) } },\n",
    "        { name: 'immutable greedy nb', f: () => { JSON.parse(raw, immutableGreedyReviver) } },\n",
    "    ]\n",
    "    \n",
    "    for(var test of tests) {\n",
    "        mean = runTrials(test.f, 100)\n",
    "        console.log(_.padEnd(test.name, 50), mean.toString().slice(0,10), 'ms')\n",
    "    }\n",
    "}"
   ],
   "outputs": [
    {
     "name": "stdout",
     "text": [
      "\n",
      " -----  altair.ipynb\n",
      "straight JSON.parse baseline                       1.10996391 ms\n",
      "Object.freeze baseline                             2.29745900 ms\n",
      "straight JSON.parse then commutable conversion     6.84918417 ms\n",
      "immutable greedy nb                                5.85418076 ms\n",
      "\n",
      " -----  display-updates.ipynb\n",
      "straight JSON.parse baseline                       0.05840098 ms\n",
      "Object.freeze baseline                             0.26966722 ms\n",
      "straight JSON.parse then commutable conversion     0.84944353 ms\n",
      "immutable greedy nb                                0.46853360 ms\n",
      "\n",
      " -----  download-stats.ipynb\n",
      "straight JSON.parse baseline                       0.03572858 ms\n",
      "Object.freeze baseline                             0.17462950 ms\n",
      "straight JSON.parse then commutable conversion     0.31322497 ms\n",
      "immutable greedy nb                                0.24238763 ms\n",
      "\n",
      " -----  geojson.ipynb\n",
      "straight JSON.parse baseline                       0.05013291 ms\n",
      "Object.freeze baseline                             0.14327998 ms\n",
      "straight JSON.parse then commutable conversion     0.58083947 ms\n",
      "immutable greedy nb                                0.33833036 ms\n",
      "\n",
      " -----  immutable-revival.ipynb\n",
      "straight JSON.parse baseline                       0.15673182 ms\n",
      "Object.freeze baseline                             0.31573414 ms\n",
      "straight JSON.parse then commutable conversion     1.14495437 ms\n",
      "immutable greedy nb                                0.58035187 ms\n",
      "\n",
      " -----  intro.ipynb\n",
      "straight JSON.parse baseline                       0.03714253 ms\n",
      "Object.freeze baseline                             0.12131342 ms\n",
      "straight JSON.parse then commutable conversion     0.28494391 ms\n",
      "immutable greedy nb                                0.28878687 ms\n",
      "\n",
      " -----  pandas-to-geojson.ipynb\n",
      "straight JSON.parse baseline                       0.08702977 ms\n",
      "Object.freeze baseline                             0.31443576 ms\n",
      "straight JSON.parse then commutable conversion     1.02646198 ms\n",
      "immutable greedy nb                                0.55603610 ms\n",
      "\n",
      " -----  plotly.ipynb\n",
      "straight JSON.parse baseline                       0.69402044 ms\n",
      "Object.freeze baseline                             1.88442499 ms\n",
      "straight JSON.parse then commutable conversion     2.04263001 ms\n",
      "immutable greedy nb                                2.31509689 ms\n",
      "\n",
      " -----  plotlyr.ipynb\n",
      "straight JSON.parse baseline                       0.00980337 ms\n",
      "Object.freeze baseline                             0.03465727 ms\n",
      "straight JSON.parse then commutable conversion     0.10106241 ms\n",
      "immutable greedy nb                                0.05404440 ms\n"
     ],
     "output_type": "stream"
    }
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since these are in milliseconds and the difference is not much, it seems like maybe this doesn't need to be optimized. In the case of the altair notebook, which has a pretty big JSON structure inside (and only one!), perhaps it would make sense if some of our structure is frozen objects (don't force vega payloads to be Immutable Maps).\n",
    "\n",
    "```\n",
    " -----  altair.ipynb\n",
    "straight JSON.parse baseline                       1.10996391 ms\n",
    "Object.freeze baseline                             2.29745900 ms\n",
    "straight JSON.parse then commutable conversion     6.84918417 ms\n",
    "immutable greedy nb                                5.85418076 ms\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false,
    "outputExpanded": false
   },
   "source": [],
   "outputs": []
  }
 ],
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "kernelspec": {
   "name": "node_nteract",
   "language": "javascript",
   "display_name": "Node.js (nteract)"
  },
  "kernel_info": {
   "name": "node_nteract"
  },
  "language_info": {
   "name": "javascript",
   "version": "6.5.0",
   "mimetype": "application/javascript",
   "file_extension": ".js"
  }
 }
}
{'fraction_non_alphanumeric': 0.1766599597585513, 'fraction_numerical': 0.05535434831209479, 'mean_word_length': 1.835446247464503, 'pattern_counts': {'":': 226, '<': 5, '<?xml version=': 0, '>': 23, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 3, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4369567', 'n_tokens_mistral': 8461, 'n_tokens_neox': 7615, 'n_words': 1731}