Search code examples
javascriptperformanceprofilingv8pyv8

Profiling Javascript in PyV8


I have a JS codebase running within PyV8. Now I'd like to improve its performance, but there don't seem to be any hooks to enable the V8 profiler. In an older trunk version of PyV8 there are some options referencing the profiler but I don't find any documentation on it. Do you have any idea on how to profile in PyV8 without me having to rewrite the Python-to-JS wrapper?

Do you know of any JS-only-framework that uses monkey patching in order to generate a timing profile? It's not a big deal if there is some overhead involved - better than not having a profile at all.


Solution

  • At the end I've found the hint I needed in the book 'Pro Javascript Design Patterns': Use a closure together with func.apply to apply instrumentation on functions. Unfortunately, the JS way of decorating functions is not quite as clean as Python's - but hey, it works and I get the information I need to drill down into the code's performance characteristics.

    profile.js

    function mod_profiler() {
        var profile_by_function_name = {};
        var total_time = 0;
        var time_counting_for_function = null;
    
        function get_function_name(func) {
            var result = func.toString();
            result = result.substr('function '.length);
            result = result.substr(0, result.indexOf('('));
            return result;
        }
    
        function update_profile(function_name, elapsed_time) {
            var profile = profile_by_function_name[function_name];
            if (profile === undefined) {
                profile = {calls:0, elapsed_time:0};
                profile_by_function_name[function_name] = profile;
            }
            profile.calls += 1;
            profile.elapsed_time += elapsed_time;
            if (time_counting_for_function === function_name) {
                total_time += elapsed_time;
            }
        }
    
        function profile(func) {
            function profiled() {
                var function_name = get_function_name(func);
                if (time_counting_for_function === null) {
                    time_counting_for_function = function_name;
                }
                var start_time = new Date().getTime()
                var result = func.apply(undefined, arguments);
                var elapsed_time = new Date().getTime() - start_time;
                update_profile(function_name, elapsed_time);
                if (time_counting_for_function === function_name) {
                    time_counting_for_function = null;
                }
                return result;
            }
            return profiled;
        }
    
        function get_formatted_result() {
            function get_whitespace(length) {
                var result = "";
                for (var i = 0; i < length; i++) {
                    result += " ";
                }
                return result;
            }
    
            var function_names = Object.keys(profile_by_function_name);
            var function_names_sorted_by_elapsed_time = function_names.sort(function (a,b) {
                var elapsed_a = profile_by_function_name[a].elapsed_time;
                var elapsed_b = profile_by_function_name[b].elapsed_time;
                if (elapsed_a < elapsed_b) {
                    return 1;
                }
                if (elapsed_a > elapsed_b) {
                    return -1;
                }
                return 0;
            });
            var max_name_length = 0;
            for (var i = 0; i < function_names_sorted_by_elapsed_time.length; i++) {
                if (function_names_sorted_by_elapsed_time[i].length > max_name_length) {
                    max_name_length = function_names_sorted_by_elapsed_time[i].length;
                }
            }
            var result = "\n" + get_whitespace(max_name_length) + " " + "#calls\telapsed\t% of total\n";
            for (var i = 0; i < function_names_sorted_by_elapsed_time.length; i++) {
                if (total_time === 0) {
                    break;
                }
                var function_name = function_names_sorted_by_elapsed_time[i];
                var percentage_elapsed = profile_by_function_name[function_name].elapsed_time * 100 / total_time;
                if (percentage_elapsed < 0.3) {
                    break;
                }
                result += function_name + ":" + get_whitespace(max_name_length - 1 - function_name.length) + profile_by_function_name[function_name].calls + "\t" + profile_by_function_name[function_name].elapsed_time + "\t" + percentage_elapsed.toFixed(2) + "\n";
            }
            result += "==========\n";
            result += "total time accounted for [ms]: " + total_time;
            return result;
        }
    
        return {
            profile: profile,
            get_formatted_result: get_formatted_result
        }
    }
    

    my_module_1.js

    function mod_1(profiler_param) {
        var profiler = profiler_param;
    
        function my_private_func() {
            return "hello world2";
        }
        if (typeof(profiler) === 'object' && profiler !== null) {
        render_user_menu = profiler.profile(render_user_menu);
    } //private functions need the instrumentation to be added manually or else they're not included in the profiling.
    
        function my_public_func1() {
            return "hello world";
        }
    
        function my_public_func2(input1, input2) {
            return my_private_func() + input1 + input2;
        }
    
        //public functions get the instrumentations automatically as long as they're called from outside the module
        var public_function_by_names = {
            my_public_func1: my_public_func1
            my_public_func2: my_public_func2
        }
    
        var result = {};
        var public_function_names = Object.keys(public_function_by_names);
        for (var i = 0; i < public_function_names.length; i++) {
            var func = public_function_by_names[public_function_names[i]];
            if (typeof(profiler) === 'object' && profiler !== null) {
                result[public_function_names[i]] = profiler.profile(func);
            }
            else {
                result[public_function_names[i]] = func;
            }
        }
        return result;
    }
    

    PyV8 side

    with X4GEJSContext(extensions=['profile', 'my_module_1']) as ctx:
        if self.enable_profiling == True:
        ctx.eval("var profiler = mod_profiler();")
            ctx.eval("var mod1 = mod_1(profiler);")
            #note: you can pass profiler to as many modules as you want and they get instrumented together.
            logging.info(ctx.eval("mod1.my_public_func_1() + mod1.my_public_func_2('a', 3);"))
            logging.info(ctx.eval("profiler.get_formatted_result();"))
        else:
            ctx.eval("var mod1 = mod_1();") #it still works without the profiler
    

    Output

    "hello worldhelloworld2a3"
                                                                                     #calls elapsed % of total
      my_public_func1:                                                                 1    31  50.00
      my_public_func2:                                                                 1    31  50.00
      my_private_func:                                                                 1    31  50.00
    
      ==========
      total time accounted for [ms]: 62