Expression Profiling with Sharrow #937

Description

@jpn--

The current expression profiler added in #936 can measure the runtime performance of spec file expressions in legacy mode by observing the elapsed time for evaluating each expression. This is called instrumentation, as we are adding instruments directly to the code being observed, to monitor the runtime of each expression. This is effective because expressions are evaluated in a serial fashion: all the work to evaluate a single expression across all choosers is completed before the work to evaluate the next expression begins. Therefore, the number of time observations needed to complete the profiling increases only linearly with the number of expressions, and is independent of the number of choosers.

This profiler does not work with sharrow.

Why? Because the sharrow code evaluates spec files differently: at least from the perspective of the raw code itself, choosers are evaluated serially, with all expressions for each chooser evaluated at once, before moving on to the next chooser. This may not even hold exactly after compilation, as the numba compiler may optimize the code by processing data in a different order or by handling several choosers simultaneously. Moreover, if timing code is written into numba compiled expressions, it is possible that the compiler would optimize away the timing itself (i.e., it could detect that the substantive code is completely independent of the timing code, and “complete” the timing instructions before completing the substantive instructions). Disabling the optimization to prevent this could also disable some of the substantive optimizations that allow sharrow to run efficiently, foiling the purpose of the profiling.

Moreover, even if those code optimizations are disabled to ensure processing is sequential, the number of time observations required increases with the product of the number of expressions and the number of choosers, imposing a potentially massive overhead on profiling that will likely distort the results. This overhead might be mitigated or avoided by using a statistical sampler instead of instrumentation. Profiling via sampling involves having an outside tool peek into the running process at regular (frequent) intervals and see which what is happening. This is how the current ActivitySim memory profiling works, although profiling gross memory usage is easier than profiling compiled code. There is a statistical profiling tool for numba called “Profila”, https://github.com/pythonspeed/profila. However, this tool is useful only for single-process (non-parallelized) code, and more importantly, only runs on Linux. It is primarily supported by a single developer, who appears to be actively working on it, and who does show some interest in being able to port the tool to other platforms, but not Windows. Given that most ActivitySim users run on Windows and do not want to work in Linux, this tool seems undesirable.

Design Options for Sharrow Profiler

Given the complexity, the expected overhead induced, and the likelihood that attempting to directly measure expression runtime from inside a sharrow component will fundamentally change the runtimes being measured, it is not a recommended solution. As an alternative, we propose to measure sharrow expression runtime from outside of sharrow, or more specifically from outside of the numba compiled portions of sharrow. This could be approached one of two ways:

  1. Compile a separate sharrow flow for each single expression in a spec file, and externally measure the runtime for each flow.
  2. Compile a separate sharrow flow for each marginal expression in a spec file, and externally measure the marginal increase in runtime for each flow.

Either of these approaches would move the runtime measurement outside of the numba compiler into interpreted Python code, which would help solve many of the issues described above. However, each comes with drawbacks.

Profiling by Expression

Profiling the sharrow-compiled spec files by writing separate sharrow-compiled pieces for each individual expression is appealing, because it would seem to give the cleanest view into the runtime for each expression. It also avoids the risk of weird results that might be difficult for less-skilled users to interpret (e.g. negative runtimes). There is some potential complexity to implement this, as spec files can include temporary values that are cross-referenced in subsequent expressions, and these values would not be internally available to sharrow when running with expressions as singletons. It would be possible to feed these expressions to the sharrow process externally by pre-computing the values and adding them to a dictionary of inputs, but this is not exactly the same as having cached temporaries already available inside the compiled code, and the runtime implications of this are unknown.

Profiling by Marginal Expression

Under this approach, sharrow spec files would be constructed where each expression is evaluated in a compiled context together with all the expressions that come before it. The marginal runtime would then be observed by comparing against the runtime of the previous test, which included one fewer expression. This approach has a distinct advantage over the one-at-a-time approach, in that it is measuring an overall runtime in a context that is more similar to the ultimate result (i.e. running the complete spec file). This will allow the compiler to employ a wider set of optimizations, more like the optimizations used for the full spec, which is ultimately the question of interest.

This approach does have some downsides as well. First, because runtimes will be calculated by taking the difference of two different results, there is twice as much opportunity for exogenous factors (other processes, thermal throttling, etc) to significantly skew the results. Second, overall runtimes always have some small amounts of stochasticity to them, so profiling will only be reliable if undertaken on datasets that are large enough that the signal will overwhelm this noise. Third, the total runtime needed to do the profiling will grow in proportion to the square of the number of expressions, instead of simply linear growth. This is probably not a serious problem as profiling will only be done infrequently, but exceedingly long timelines to complete profiling results can be annoying nonetheless.

Which approach is better?

It is unclear to me at this time which approach is better. I welcome input from the ActivitySim community if anyone strongly believes that one is superior, based on evidence or experience from other projects. Absent such insight, the best way to evaluate these two approaches may be to implement them both and do some testing. Ultimately they are not extremely different, so implementing both will not require anywhere near double the resources of implementing only one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FeatureNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Expression Profiling with Sharrow #937

      Description

      @jpn--

      The current expression profiler added in #936 can measure the runtime performance of spec file expressions in legacy mode by observing the elapsed time for evaluating each expression. This is called instrumentation, as we are adding instruments directly to the code being observed, to monitor the runtime of each expression. This is effective because expressions are evaluated in a serial fashion: all the work to evaluate a single expression across all choosers is completed before the work to evaluate the next expression begins. Therefore, the number of time observations needed to complete the profiling increases only linearly with the number of expressions, and is independent of the number of choosers.

      This profiler does not work with sharrow.

      Why? Because the sharrow code evaluates spec files differently: at least from the perspective of the raw code itself, choosers are evaluated serially, with all expressions for each chooser evaluated at once, before moving on to the next chooser. This may not even hold exactly after compilation, as the numba compiler may optimize the code by processing data in a different order or by handling several choosers simultaneously. Moreover, if timing code is written into numba compiled expressions, it is possible that the compiler would optimize away the timing itself (i.e., it could detect that the substantive code is completely independent of the timing code, and “complete” the timing instructions before completing the substantive instructions). Disabling the optimization to prevent this could also disable some of the substantive optimizations that allow sharrow to run efficiently, foiling the purpose of the profiling.

      Moreover, even if those code optimizations are disabled to ensure processing is sequential, the number of time observations required increases with the product of the number of expressions and the number of choosers, imposing a potentially massive overhead on profiling that will likely distort the results. This overhead might be mitigated or avoided by using a statistical sampler instead of instrumentation. Profiling via sampling involves having an outside tool peek into the running process at regular (frequent) intervals and see which what is happening. This is how the current ActivitySim memory profiling works, although profiling gross memory usage is easier than profiling compiled code. There is a statistical profiling tool for numba called “Profila”, https://github.com/pythonspeed/profila. However, this tool is useful only for single-process (non-parallelized) code, and more importantly, only runs on Linux. It is primarily supported by a single developer, who appears to be actively working on it, and who does show some interest in being able to port the tool to other platforms, but not Windows. Given that most ActivitySim users run on Windows and do not want to work in Linux, this tool seems undesirable.

      Design Options for Sharrow Profiler

      Given the complexity, the expected overhead induced, and the likelihood that attempting to directly measure expression runtime from inside a sharrow component will fundamentally change the runtimes being measured, it is not a recommended solution. As an alternative, we propose to measure sharrow expression runtime from outside of sharrow, or more specifically from outside of the numba compiled portions of sharrow. This could be approached one of two ways:

      1. Compile a separate sharrow flow for each single expression in a spec file, and externally measure the runtime for each flow.
      2. Compile a separate sharrow flow for each marginal expression in a spec file, and externally measure the marginal increase in runtime for each flow.

      Either of these approaches would move the runtime measurement outside of the numba compiler into interpreted Python code, which would help solve many of the issues described above. However, each comes with drawbacks.

      Profiling by Expression

      Profiling the sharrow-compiled spec files by writing separate sharrow-compiled pieces for each individual expression is appealing, because it would seem to give the cleanest view into the runtime for each expression. It also avoids the risk of weird results that might be difficult for less-skilled users to interpret (e.g. negative runtimes). There is some potential complexity to implement this, as spec files can include temporary values that are cross-referenced in subsequent expressions, and these values would not be internally available to sharrow when running with expressions as singletons. It would be possible to feed these expressions to the sharrow process externally by pre-computing the values and adding them to a dictionary of inputs, but this is not exactly the same as having cached temporaries already available inside the compiled code, and the runtime implications of this are unknown.

      Profiling by Marginal Expression

      Under this approach, sharrow spec files would be constructed where each expression is evaluated in a compiled context together with all the expressions that come before it. The marginal runtime would then be observed by comparing against the runtime of the previous test, which included one fewer expression. This approach has a distinct advantage over the one-at-a-time approach, in that it is measuring an overall runtime in a context that is more similar to the ultimate result (i.e. running the complete spec file). This will allow the compiler to employ a wider set of optimizations, more like the optimizations used for the full spec, which is ultimately the question of interest.

      This approach does have some downsides as well. First, because runtimes will be calculated by taking the difference of two different results, there is twice as much opportunity for exogenous factors (other processes, thermal throttling, etc) to significantly skew the results. Second, overall runtimes always have some small amounts of stochasticity to them, so profiling will only be reliable if undertaken on datasets that are large enough that the signal will overwhelm this noise. Third, the total runtime needed to do the profiling will grow in proportion to the square of the number of expressions, instead of simply linear growth. This is probably not a serious problem as profiling will only be done infrequently, but exceedingly long timelines to complete profiling results can be annoying nonetheless.

      Which approach is better?

      It is unclear to me at this time which approach is better. I welcome input from the ActivitySim community if anyone strongly believes that one is superior, based on evidence or experience from other projects. Absent such insight, the best way to evaluate these two approaches may be to implement them both and do some testing. Ultimately they are not extremely different, so implementing both will not require anywhere near double the resources of implementing only one.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        FeatureNew feature or request

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Expression Profiling with Sharrow #937

          Description

          @jpn--

          The current expression profiler added in #936 can measure the runtime performance of spec file expressions in legacy mode by observing the elapsed time for evaluating each expression. This is called instrumentation, as we are adding instruments directly to the code being observed, to monitor the runtime of each expression. This is effective because expressions are evaluated in a serial fashion: all the work to evaluate a single expression across all choosers is completed before the work to evaluate the next expression begins. Therefore, the number of time observations needed to complete the profiling increases only linearly with the number of expressions, and is independent of the number of choosers.

          This profiler does not work with sharrow.

          Why? Because the sharrow code evaluates spec files differently: at least from the perspective of the raw code itself, choosers are evaluated serially, with all expressions for each chooser evaluated at once, before moving on to the next chooser. This may not even hold exactly after compilation, as the numba compiler may optimize the code by processing data in a different order or by handling several choosers simultaneously. Moreover, if timing code is written into numba compiled expressions, it is possible that the compiler would optimize away the timing itself (i.e., it could detect that the substantive code is completely independent of the timing code, and “complete” the timing instructions before completing the substantive instructions). Disabling the optimization to prevent this could also disable some of the substantive optimizations that allow sharrow to run efficiently, foiling the purpose of the profiling.

          Moreover, even if those code optimizations are disabled to ensure processing is sequential, the number of time observations required increases with the product of the number of expressions and the number of choosers, imposing a potentially massive overhead on profiling that will likely distort the results. This overhead might be mitigated or avoided by using a statistical sampler instead of instrumentation. Profiling via sampling involves having an outside tool peek into the running process at regular (frequent) intervals and see which what is happening. This is how the current ActivitySim memory profiling works, although profiling gross memory usage is easier than profiling compiled code. There is a statistical profiling tool for numba called “Profila”, https://github.com/pythonspeed/profila. However, this tool is useful only for single-process (non-parallelized) code, and more importantly, only runs on Linux. It is primarily supported by a single developer, who appears to be actively working on it, and who does show some interest in being able to port the tool to other platforms, but not Windows. Given that most ActivitySim users run on Windows and do not want to work in Linux, this tool seems undesirable.

          Design Options for Sharrow Profiler

          Given the complexity, the expected overhead induced, and the likelihood that attempting to directly measure expression runtime from inside a sharrow component will fundamentally change the runtimes being measured, it is not a recommended solution. As an alternative, we propose to measure sharrow expression runtime from outside of sharrow, or more specifically from outside of the numba compiled portions of sharrow. This could be approached one of two ways:

          1. Compile a separate sharrow flow for each single expression in a spec file, and externally measure the runtime for each flow.
          2. Compile a separate sharrow flow for each marginal expression in a spec file, and externally measure the marginal increase in runtime for each flow.

          Either of these approaches would move the runtime measurement outside of the numba compiler into interpreted Python code, which would help solve many of the issues described above. However, each comes with drawbacks.

          Profiling by Expression

          Profiling the sharrow-compiled spec files by writing separate sharrow-compiled pieces for each individual expression is appealing, because it would seem to give the cleanest view into the runtime for each expression. It also avoids the risk of weird results that might be difficult for less-skilled users to interpret (e.g. negative runtimes). There is some potential complexity to implement this, as spec files can include temporary values that are cross-referenced in subsequent expressions, and these values would not be internally available to sharrow when running with expressions as singletons. It would be possible to feed these expressions to the sharrow process externally by pre-computing the values and adding them to a dictionary of inputs, but this is not exactly the same as having cached temporaries already available inside the compiled code, and the runtime implications of this are unknown.

          Profiling by Marginal Expression

          Under this approach, sharrow spec files would be constructed where each expression is evaluated in a compiled context together with all the expressions that come before it. The marginal runtime would then be observed by comparing against the runtime of the previous test, which included one fewer expression. This approach has a distinct advantage over the one-at-a-time approach, in that it is measuring an overall runtime in a context that is more similar to the ultimate result (i.e. running the complete spec file). This will allow the compiler to employ a wider set of optimizations, more like the optimizations used for the full spec, which is ultimately the question of interest.

          This approach does have some downsides as well. First, because runtimes will be calculated by taking the difference of two different results, there is twice as much opportunity for exogenous factors (other processes, thermal throttling, etc) to significantly skew the results. Second, overall runtimes always have some small amounts of stochasticity to them, so profiling will only be reliable if undertaken on datasets that are large enough that the signal will overwhelm this noise. Third, the total runtime needed to do the profiling will grow in proportion to the square of the number of expressions, instead of simply linear growth. This is probably not a serious problem as profiling will only be done infrequently, but exceedingly long timelines to complete profiling results can be annoying nonetheless.

          Which approach is better?

          It is unclear to me at this time which approach is better. I welcome input from the ActivitySim community if anyone strongly believes that one is superior, based on evidence or experience from other projects. Absent such insight, the best way to evaluate these two approaches may be to implement them both and do some testing. Ultimately they are not extremely different, so implementing both will not require anywhere near double the resources of implementing only one.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            FeatureNew feature or request

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Expression Profiling with Sharrow #937

              Description

              @jpn--

              The current expression profiler added in #936 can measure the runtime performance of spec file expressions in legacy mode by observing the elapsed time for evaluating each expression. This is called instrumentation, as we are adding instruments directly to the code being observed, to monitor the runtime of each expression. This is effective because expressions are evaluated in a serial fashion: all the work to evaluate a single expression across all choosers is completed before the work to evaluate the next expression begins. Therefore, the number of time observations needed to complete the profiling increases only linearly with the number of expressions, and is independent of the number of choosers.

              This profiler does not work with sharrow.

              Why? Because the sharrow code evaluates spec files differently: at least from the perspective of the raw code itself, choosers are evaluated serially, with all expressions for each chooser evaluated at once, before moving on to the next chooser. This may not even hold exactly after compilation, as the numba compiler may optimize the code by processing data in a different order or by handling several choosers simultaneously. Moreover, if timing code is written into numba compiled expressions, it is possible that the compiler would optimize away the timing itself (i.e., it could detect that the substantive code is completely independent of the timing code, and “complete” the timing instructions before completing the substantive instructions). Disabling the optimization to prevent this could also disable some of the substantive optimizations that allow sharrow to run efficiently, foiling the purpose of the profiling.

              Moreover, even if those code optimizations are disabled to ensure processing is sequential, the number of time observations required increases with the product of the number of expressions and the number of choosers, imposing a potentially massive overhead on profiling that will likely distort the results. This overhead might be mitigated or avoided by using a statistical sampler instead of instrumentation. Profiling via sampling involves having an outside tool peek into the running process at regular (frequent) intervals and see which what is happening. This is how the current ActivitySim memory profiling works, although profiling gross memory usage is easier than profiling compiled code. There is a statistical profiling tool for numba called “Profila”, https://github.com/pythonspeed/profila. However, this tool is useful only for single-process (non-parallelized) code, and more importantly, only runs on Linux. It is primarily supported by a single developer, who appears to be actively working on it, and who does show some interest in being able to port the tool to other platforms, but not Windows. Given that most ActivitySim users run on Windows and do not want to work in Linux, this tool seems undesirable.

              Design Options for Sharrow Profiler

              Given the complexity, the expected overhead induced, and the likelihood that attempting to directly measure expression runtime from inside a sharrow component will fundamentally change the runtimes being measured, it is not a recommended solution. As an alternative, we propose to measure sharrow expression runtime from outside of sharrow, or more specifically from outside of the numba compiled portions of sharrow. This could be approached one of two ways:

              1. Compile a separate sharrow flow for each single expression in a spec file, and externally measure the runtime for each flow.
              2. Compile a separate sharrow flow for each marginal expression in a spec file, and externally measure the marginal increase in runtime for each flow.

              Either of these approaches would move the runtime measurement outside of the numba compiler into interpreted Python code, which would help solve many of the issues described above. However, each comes with drawbacks.

              Profiling by Expression

              Profiling the sharrow-compiled spec files by writing separate sharrow-compiled pieces for each individual expression is appealing, because it would seem to give the cleanest view into the runtime for each expression. It also avoids the risk of weird results that might be difficult for less-skilled users to interpret (e.g. negative runtimes). There is some potential complexity to implement this, as spec files can include temporary values that are cross-referenced in subsequent expressions, and these values would not be internally available to sharrow when running with expressions as singletons. It would be possible to feed these expressions to the sharrow process externally by pre-computing the values and adding them to a dictionary of inputs, but this is not exactly the same as having cached temporaries already available inside the compiled code, and the runtime implications of this are unknown.

              Profiling by Marginal Expression

              Under this approach, sharrow spec files would be constructed where each expression is evaluated in a compiled context together with all the expressions that come before it. The marginal runtime would then be observed by comparing against the runtime of the previous test, which included one fewer expression. This approach has a distinct advantage over the one-at-a-time approach, in that it is measuring an overall runtime in a context that is more similar to the ultimate result (i.e. running the complete spec file). This will allow the compiler to employ a wider set of optimizations, more like the optimizations used for the full spec, which is ultimately the question of interest.

              This approach does have some downsides as well. First, because runtimes will be calculated by taking the difference of two different results, there is twice as much opportunity for exogenous factors (other processes, thermal throttling, etc) to significantly skew the results. Second, overall runtimes always have some small amounts of stochasticity to them, so profiling will only be reliable if undertaken on datasets that are large enough that the signal will overwhelm this noise. Third, the total runtime needed to do the profiling will grow in proportion to the square of the number of expressions, instead of simply linear growth. This is probably not a serious problem as profiling will only be done infrequently, but exceedingly long timelines to complete profiling results can be annoying nonetheless.

              Which approach is better?

              It is unclear to me at this time which approach is better. I welcome input from the ActivitySim community if anyone strongly believes that one is superior, based on evidence or experience from other projects. Absent such insight, the best way to evaluate these two approaches may be to implement them both and do some testing. Ultimately they are not extremely different, so implementing both will not require anywhere near double the resources of implementing only one.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                FeatureNew feature or request

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Expression Profiling with Sharrow #937

                  Description

                  @jpn--

                  The current expression profiler added in #936 can measure the runtime performance of spec file expressions in legacy mode by observing the elapsed time for evaluating each expression. This is called instrumentation, as we are adding instruments directly to the code being observed, to monitor the runtime of each expression. This is effective because expressions are evaluated in a serial fashion: all the work to evaluate a single expression across all choosers is completed before the work to evaluate the next expression begins. Therefore, the number of time observations needed to complete the profiling increases only linearly with the number of expressions, and is independent of the number of choosers.

                  This profiler does not work with sharrow.

                  Why? Because the sharrow code evaluates spec files differently: at least from the perspective of the raw code itself, choosers are evaluated serially, with all expressions for each chooser evaluated at once, before moving on to the next chooser. This may not even hold exactly after compilation, as the numba compiler may optimize the code by processing data in a different order or by handling several choosers simultaneously. Moreover, if timing code is written into numba compiled expressions, it is possible that the compiler would optimize away the timing itself (i.e., it could detect that the substantive code is completely independent of the timing code, and “complete” the timing instructions before completing the substantive instructions). Disabling the optimization to prevent this could also disable some of the substantive optimizations that allow sharrow to run efficiently, foiling the purpose of the profiling.

                  Moreover, even if those code optimizations are disabled to ensure processing is sequential, the number of time observations required increases with the product of the number of expressions and the number of choosers, imposing a potentially massive overhead on profiling that will likely distort the results. This overhead might be mitigated or avoided by using a statistical sampler instead of instrumentation. Profiling via sampling involves having an outside tool peek into the running process at regular (frequent) intervals and see which what is happening. This is how the current ActivitySim memory profiling works, although profiling gross memory usage is easier than profiling compiled code. There is a statistical profiling tool for numba called “Profila”, https://github.com/pythonspeed/profila. However, this tool is useful only for single-process (non-parallelized) code, and more importantly, only runs on Linux. It is primarily supported by a single developer, who appears to be actively working on it, and who does show some interest in being able to port the tool to other platforms, but not Windows. Given that most ActivitySim users run on Windows and do not want to work in Linux, this tool seems undesirable.

                  Design Options for Sharrow Profiler

                  Given the complexity, the expected overhead induced, and the likelihood that attempting to directly measure expression runtime from inside a sharrow component will fundamentally change the runtimes being measured, it is not a recommended solution. As an alternative, we propose to measure sharrow expression runtime from outside of sharrow, or more specifically from outside of the numba compiled portions of sharrow. This could be approached one of two ways:

                  1. Compile a separate sharrow flow for each single expression in a spec file, and externally measure the runtime for each flow.
                  2. Compile a separate sharrow flow for each marginal expression in a spec file, and externally measure the marginal increase in runtime for each flow.

                  Either of these approaches would move the runtime measurement outside of the numba compiler into interpreted Python code, which would help solve many of the issues described above. However, each comes with drawbacks.

                  Profiling by Expression

                  Profiling the sharrow-compiled spec files by writing separate sharrow-compiled pieces for each individual expression is appealing, because it would seem to give the cleanest view into the runtime for each expression. It also avoids the risk of weird results that might be difficult for less-skilled users to interpret (e.g. negative runtimes). There is some potential complexity to implement this, as spec files can include temporary values that are cross-referenced in subsequent expressions, and these values would not be internally available to sharrow when running with expressions as singletons. It would be possible to feed these expressions to the sharrow process externally by pre-computing the values and adding them to a dictionary of inputs, but this is not exactly the same as having cached temporaries already available inside the compiled code, and the runtime implications of this are unknown.

                  Profiling by Marginal Expression

                  Under this approach, sharrow spec files would be constructed where each expression is evaluated in a compiled context together with all the expressions that come before it. The marginal runtime would then be observed by comparing against the runtime of the previous test, which included one fewer expression. This approach has a distinct advantage over the one-at-a-time approach, in that it is measuring an overall runtime in a context that is more similar to the ultimate result (i.e. running the complete spec file). This will allow the compiler to employ a wider set of optimizations, more like the optimizations used for the full spec, which is ultimately the question of interest.

                  This approach does have some downsides as well. First, because runtimes will be calculated by taking the difference of two different results, there is twice as much opportunity for exogenous factors (other processes, thermal throttling, etc) to significantly skew the results. Second, overall runtimes always have some small amounts of stochasticity to them, so profiling will only be reliable if undertaken on datasets that are large enough that the signal will overwhelm this noise. Third, the total runtime needed to do the profiling will grow in proportion to the square of the number of expressions, instead of simply linear growth. This is probably not a serious problem as profiling will only be done infrequently, but exceedingly long timelines to complete profiling results can be annoying nonetheless.

                  Which approach is better?

                  It is unclear to me at this time which approach is better. I welcome input from the ActivitySim community if anyone strongly believes that one is superior, based on evidence or experience from other projects. Absent such insight, the best way to evaluate these two approaches may be to implement them both and do some testing. Ultimately they are not extremely different, so implementing both will not require anywhere near double the resources of implementing only one.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    FeatureNew feature or request

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Expression Profiling with Sharrow #937

                      Description

                      @jpn--

                      The current expression profiler added in #936 can measure the runtime performance of spec file expressions in legacy mode by observing the elapsed time for evaluating each expression. This is called instrumentation, as we are adding instruments directly to the code being observed, to monitor the runtime of each expression. This is effective because expressions are evaluated in a serial fashion: all the work to evaluate a single expression across all choosers is completed before the work to evaluate the next expression begins. Therefore, the number of time observations needed to complete the profiling increases only linearly with the number of expressions, and is independent of the number of choosers.

                      This profiler does not work with sharrow.

                      Why? Because the sharrow code evaluates spec files differently: at least from the perspective of the raw code itself, choosers are evaluated serially, with all expressions for each chooser evaluated at once, before moving on to the next chooser. This may not even hold exactly after compilation, as the numba compiler may optimize the code by processing data in a different order or by handling several choosers simultaneously. Moreover, if timing code is written into numba compiled expressions, it is possible that the compiler would optimize away the timing itself (i.e., it could detect that the substantive code is completely independent of the timing code, and “complete” the timing instructions before completing the substantive instructions). Disabling the optimization to prevent this could also disable some of the substantive optimizations that allow sharrow to run efficiently, foiling the purpose of the profiling.

                      Moreover, even if those code optimizations are disabled to ensure processing is sequential, the number of time observations required increases with the product of the number of expressions and the number of choosers, imposing a potentially massive overhead on profiling that will likely distort the results. This overhead might be mitigated or avoided by using a statistical sampler instead of instrumentation. Profiling via sampling involves having an outside tool peek into the running process at regular (frequent) intervals and see which what is happening. This is how the current ActivitySim memory profiling works, although profiling gross memory usage is easier than profiling compiled code. There is a statistical profiling tool for numba called “Profila”, https://github.com/pythonspeed/profila. However, this tool is useful only for single-process (non-parallelized) code, and more importantly, only runs on Linux. It is primarily supported by a single developer, who appears to be actively working on it, and who does show some interest in being able to port the tool to other platforms, but not Windows. Given that most ActivitySim users run on Windows and do not want to work in Linux, this tool seems undesirable.

                      Design Options for Sharrow Profiler

                      Given the complexity, the expected overhead induced, and the likelihood that attempting to directly measure expression runtime from inside a sharrow component will fundamentally change the runtimes being measured, it is not a recommended solution. As an alternative, we propose to measure sharrow expression runtime from outside of sharrow, or more specifically from outside of the numba compiled portions of sharrow. This could be approached one of two ways:

                      1. Compile a separate sharrow flow for each single expression in a spec file, and externally measure the runtime for each flow.
                      2. Compile a separate sharrow flow for each marginal expression in a spec file, and externally measure the marginal increase in runtime for each flow.

                      Either of these approaches would move the runtime measurement outside of the numba compiler into interpreted Python code, which would help solve many of the issues described above. However, each comes with drawbacks.

                      Profiling by Expression

                      Profiling the sharrow-compiled spec files by writing separate sharrow-compiled pieces for each individual expression is appealing, because it would seem to give the cleanest view into the runtime for each expression. It also avoids the risk of weird results that might be difficult for less-skilled users to interpret (e.g. negative runtimes). There is some potential complexity to implement this, as spec files can include temporary values that are cross-referenced in subsequent expressions, and these values would not be internally available to sharrow when running with expressions as singletons. It would be possible to feed these expressions to the sharrow process externally by pre-computing the values and adding them to a dictionary of inputs, but this is not exactly the same as having cached temporaries already available inside the compiled code, and the runtime implications of this are unknown.

                      Profiling by Marginal Expression

                      Under this approach, sharrow spec files would be constructed where each expression is evaluated in a compiled context together with all the expressions that come before it. The marginal runtime would then be observed by comparing against the runtime of the previous test, which included one fewer expression. This approach has a distinct advantage over the one-at-a-time approach, in that it is measuring an overall runtime in a context that is more similar to the ultimate result (i.e. running the complete spec file). This will allow the compiler to employ a wider set of optimizations, more like the optimizations used for the full spec, which is ultimately the question of interest.

                      This approach does have some downsides as well. First, because runtimes will be calculated by taking the difference of two different results, there is twice as much opportunity for exogenous factors (other processes, thermal throttling, etc) to significantly skew the results. Second, overall runtimes always have some small amounts of stochasticity to them, so profiling will only be reliable if undertaken on datasets that are large enough that the signal will overwhelm this noise. Third, the total runtime needed to do the profiling will grow in proportion to the square of the number of expressions, instead of simply linear growth. This is probably not a serious problem as profiling will only be done infrequently, but exceedingly long timelines to complete profiling results can be annoying nonetheless.

                      Which approach is better?

                      It is unclear to me at this time which approach is better. I welcome input from the ActivitySim community if anyone strongly believes that one is superior, based on evidence or experience from other projects. Absent such insight, the best way to evaluate these two approaches may be to implement them both and do some testing. Ultimately they are not extremely different, so implementing both will not require anywhere near double the resources of implementing only one.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        FeatureNew feature or request

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Expression Profiling with Sharrow #937

                          Description

                          @jpn--

                          The current expression profiler added in #936 can measure the runtime performance of spec file expressions in legacy mode by observing the elapsed time for evaluating each expression. This is called instrumentation, as we are adding instruments directly to the code being observed, to monitor the runtime of each expression. This is effective because expressions are evaluated in a serial fashion: all the work to evaluate a single expression across all choosers is completed before the work to evaluate the next expression begins. Therefore, the number of time observations needed to complete the profiling increases only linearly with the number of expressions, and is independent of the number of choosers.

                          This profiler does not work with sharrow.

                          Why? Because the sharrow code evaluates spec files differently: at least from the perspective of the raw code itself, choosers are evaluated serially, with all expressions for each chooser evaluated at once, before moving on to the next chooser. This may not even hold exactly after compilation, as the numba compiler may optimize the code by processing data in a different order or by handling several choosers simultaneously. Moreover, if timing code is written into numba compiled expressions, it is possible that the compiler would optimize away the timing itself (i.e., it could detect that the substantive code is completely independent of the timing code, and “complete” the timing instructions before completing the substantive instructions). Disabling the optimization to prevent this could also disable some of the substantive optimizations that allow sharrow to run efficiently, foiling the purpose of the profiling.

                          Moreover, even if those code optimizations are disabled to ensure processing is sequential, the number of time observations required increases with the product of the number of expressions and the number of choosers, imposing a potentially massive overhead on profiling that will likely distort the results. This overhead might be mitigated or avoided by using a statistical sampler instead of instrumentation. Profiling via sampling involves having an outside tool peek into the running process at regular (frequent) intervals and see which what is happening. This is how the current ActivitySim memory profiling works, although profiling gross memory usage is easier than profiling compiled code. There is a statistical profiling tool for numba called “Profila”, https://github.com/pythonspeed/profila. However, this tool is useful only for single-process (non-parallelized) code, and more importantly, only runs on Linux. It is primarily supported by a single developer, who appears to be actively working on it, and who does show some interest in being able to port the tool to other platforms, but not Windows. Given that most ActivitySim users run on Windows and do not want to work in Linux, this tool seems undesirable.

                          Design Options for Sharrow Profiler

                          Given the complexity, the expected overhead induced, and the likelihood that attempting to directly measure expression runtime from inside a sharrow component will fundamentally change the runtimes being measured, it is not a recommended solution. As an alternative, we propose to measure sharrow expression runtime from outside of sharrow, or more specifically from outside of the numba compiled portions of sharrow. This could be approached one of two ways:

                          1. Compile a separate sharrow flow for each single expression in a spec file, and externally measure the runtime for each flow.
                          2. Compile a separate sharrow flow for each marginal expression in a spec file, and externally measure the marginal increase in runtime for each flow.

                          Either of these approaches would move the runtime measurement outside of the numba compiler into interpreted Python code, which would help solve many of the issues described above. However, each comes with drawbacks.

                          Profiling by Expression

                          Profiling the sharrow-compiled spec files by writing separate sharrow-compiled pieces for each individual expression is appealing, because it would seem to give the cleanest view into the runtime for each expression. It also avoids the risk of weird results that might be difficult for less-skilled users to interpret (e.g. negative runtimes). There is some potential complexity to implement this, as spec files can include temporary values that are cross-referenced in subsequent expressions, and these values would not be internally available to sharrow when running with expressions as singletons. It would be possible to feed these expressions to the sharrow process externally by pre-computing the values and adding them to a dictionary of inputs, but this is not exactly the same as having cached temporaries already available inside the compiled code, and the runtime implications of this are unknown.

                          Profiling by Marginal Expression

                          Under this approach, sharrow spec files would be constructed where each expression is evaluated in a compiled context together with all the expressions that come before it. The marginal runtime would then be observed by comparing against the runtime of the previous test, which included one fewer expression. This approach has a distinct advantage over the one-at-a-time approach, in that it is measuring an overall runtime in a context that is more similar to the ultimate result (i.e. running the complete spec file). This will allow the compiler to employ a wider set of optimizations, more like the optimizations used for the full spec, which is ultimately the question of interest.

                          This approach does have some downsides as well. First, because runtimes will be calculated by taking the difference of two different results, there is twice as much opportunity for exogenous factors (other processes, thermal throttling, etc) to significantly skew the results. Second, overall runtimes always have some small amounts of stochasticity to them, so profiling will only be reliable if undertaken on datasets that are large enough that the signal will overwhelm this noise. Third, the total runtime needed to do the profiling will grow in proportion to the square of the number of expressions, instead of simply linear growth. This is probably not a serious problem as profiling will only be done infrequently, but exceedingly long timelines to complete profiling results can be annoying nonetheless.

                          Which approach is better?

                          It is unclear to me at this time which approach is better. I welcome input from the ActivitySim community if anyone strongly believes that one is superior, based on evidence or experience from other projects. Absent such insight, the best way to evaluate these two approaches may be to implement them both and do some testing. Ultimately they are not extremely different, so implementing both will not require anywhere near double the resources of implementing only one.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            FeatureNew feature or request

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Expression Profiling with Sharrow #937

                              Description

                              @jpn--

                              The current expression profiler added in #936 can measure the runtime performance of spec file expressions in legacy mode by observing the elapsed time for evaluating each expression. This is called instrumentation, as we are adding instruments directly to the code being observed, to monitor the runtime of each expression. This is effective because expressions are evaluated in a serial fashion: all the work to evaluate a single expression across all choosers is completed before the work to evaluate the next expression begins. Therefore, the number of time observations needed to complete the profiling increases only linearly with the number of expressions, and is independent of the number of choosers.

                              This profiler does not work with sharrow.

                              Why? Because the sharrow code evaluates spec files differently: at least from the perspective of the raw code itself, choosers are evaluated serially, with all expressions for each chooser evaluated at once, before moving on to the next chooser. This may not even hold exactly after compilation, as the numba compiler may optimize the code by processing data in a different order or by handling several choosers simultaneously. Moreover, if timing code is written into numba compiled expressions, it is possible that the compiler would optimize away the timing itself (i.e., it could detect that the substantive code is completely independent of the timing code, and “complete” the timing instructions before completing the substantive instructions). Disabling the optimization to prevent this could also disable some of the substantive optimizations that allow sharrow to run efficiently, foiling the purpose of the profiling.

                              Moreover, even if those code optimizations are disabled to ensure processing is sequential, the number of time observations required increases with the product of the number of expressions and the number of choosers, imposing a potentially massive overhead on profiling that will likely distort the results. This overhead might be mitigated or avoided by using a statistical sampler instead of instrumentation. Profiling via sampling involves having an outside tool peek into the running process at regular (frequent) intervals and see which what is happening. This is how the current ActivitySim memory profiling works, although profiling gross memory usage is easier than profiling compiled code. There is a statistical profiling tool for numba called “Profila”, https://github.com/pythonspeed/profila. However, this tool is useful only for single-process (non-parallelized) code, and more importantly, only runs on Linux. It is primarily supported by a single developer, who appears to be actively working on it, and who does show some interest in being able to port the tool to other platforms, but not Windows. Given that most ActivitySim users run on Windows and do not want to work in Linux, this tool seems undesirable.

                              Design Options for Sharrow Profiler

                              Given the complexity, the expected overhead induced, and the likelihood that attempting to directly measure expression runtime from inside a sharrow component will fundamentally change the runtimes being measured, it is not a recommended solution. As an alternative, we propose to measure sharrow expression runtime from outside of sharrow, or more specifically from outside of the numba compiled portions of sharrow. This could be approached one of two ways:

                              1. Compile a separate sharrow flow for each single expression in a spec file, and externally measure the runtime for each flow.
                              2. Compile a separate sharrow flow for each marginal expression in a spec file, and externally measure the marginal increase in runtime for each flow.

                              Either of these approaches would move the runtime measurement outside of the numba compiler into interpreted Python code, which would help solve many of the issues described above. However, each comes with drawbacks.

                              Profiling by Expression

                              Profiling the sharrow-compiled spec files by writing separate sharrow-compiled pieces for each individual expression is appealing, because it would seem to give the cleanest view into the runtime for each expression. It also avoids the risk of weird results that might be difficult for less-skilled users to interpret (e.g. negative runtimes). There is some potential complexity to implement this, as spec files can include temporary values that are cross-referenced in subsequent expressions, and these values would not be internally available to sharrow when running with expressions as singletons. It would be possible to feed these expressions to the sharrow process externally by pre-computing the values and adding them to a dictionary of inputs, but this is not exactly the same as having cached temporaries already available inside the compiled code, and the runtime implications of this are unknown.

                              Profiling by Marginal Expression

                              Under this approach, sharrow spec files would be constructed where each expression is evaluated in a compiled context together with all the expressions that come before it. The marginal runtime would then be observed by comparing against the runtime of the previous test, which included one fewer expression. This approach has a distinct advantage over the one-at-a-time approach, in that it is measuring an overall runtime in a context that is more similar to the ultimate result (i.e. running the complete spec file). This will allow the compiler to employ a wider set of optimizations, more like the optimizations used for the full spec, which is ultimately the question of interest.

                              This approach does have some downsides as well. First, because runtimes will be calculated by taking the difference of two different results, there is twice as much opportunity for exogenous factors (other processes, thermal throttling, etc) to significantly skew the results. Second, overall runtimes always have some small amounts of stochasticity to them, so profiling will only be reliable if undertaken on datasets that are large enough that the signal will overwhelm this noise. Third, the total runtime needed to do the profiling will grow in proportion to the square of the number of expressions, instead of simply linear growth. This is probably not a serious problem as profiling will only be done infrequently, but exceedingly long timelines to complete profiling results can be annoying nonetheless.

                              Which approach is better?

                              It is unclear to me at this time which approach is better. I welcome input from the ActivitySim community if anyone strongly believes that one is superior, based on evidence or experience from other projects. Absent such insight, the best way to evaluate these two approaches may be to implement them both and do some testing. Ultimately they are not extremely different, so implementing both will not require anywhere near double the resources of implementing only one.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                FeatureNew feature or request

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions