diff --git a/JSCLegacyProfiler/json2trace b/JSCLegacyProfiler/json2trace new file mode 100755 index 000000000..8dac589a7 --- /dev/null +++ b/JSCLegacyProfiler/json2trace @@ -0,0 +1,254 @@ +#!/usr/bin/env python +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import json +import smap +import trace_data +import urllib + +SECONDS_TO_NANOSECONDS = (1000*1000) +SAMPLE_DELTA_IN_SECONDS = 0.0001 + +class Marker(object): + def __init__(self, _name, _timestamp, _depth, _is_end, _ident, url, line, col): + self.name = _name + self.timestamp = _timestamp + self.depth = _depth + self.is_end = _is_end + self.ident = _ident + self.url = url + self.line = line + self.col = col + +# sort markers making sure they are ordered by timestamp then depth of function call +# and finally that markers of the same ident are sorted in the order begin then end + def __cmp__(self, other): + if self.timestamp < other.timestamp: + return -1 + if self.timestamp > other.timestamp: + return 1 + if self.depth < other.depth: + return -1 + if self.depth > other.depth: + return 1 + if self.ident == other.ident: + if self.is_end: + return 1 + return 0 + +# calculate marker name based on combination of function name and location +def _calcname(entry): + funcname = "" + if "functionName" in entry: + funcname = funcname + entry["functionName"] + return funcname + +def _calcurl(mapcache, entry, map_file): + if entry.url not in mapcache: + map_url = entry.url.replace('.bundle', '.map') + + if map_url != entry.url: + if map_file: + print('Loading sourcemap from:' + map_file) + map_url = map_file + + try: + url_file = urllib.urlopen(map_url) + if url_file != None: + entries = smap.parse(url_file) + mapcache[entry.url] = entries + except Exception, e: + mapcache[entry.url] = [] + + if entry.url in mapcache: + source_entry = smap.find(mapcache[entry.url], entry.line, entry.col) + if source_entry: + entry.url = 'file://' + source_entry.src + entry.line = source_entry.src_line + entry.col = source_entry.src_col + +def _compute_markers(markers, call_point, depth): + name = _calcname(call_point) + ident = len(markers) + url = "" + lineNumber = -1 + columnNumber = -1 + if "url" in call_point: + url = call_point["url"] + if "lineNumber" in call_point: + lineNumber = call_point["lineNumber"] + if "columnNumber" in call_point: + columnNumber = call_point["columnNumber"] + + for call in call_point["calls"]: + markers.append(Marker(name, call["startTime"], depth, 0, ident, url, lineNumber, columnNumber)) + markers.append(Marker(name, call["startTime"] + call["totalTime"], depth, 1, ident, url, lineNumber, columnNumber)) + ident = ident + 2 + if "children" in call_point: + for child in call_point["children"]: + _compute_markers(markers, child, depth+1); + +def _find_child(children, name): + for child in children: + if child['functionName'] == name: + return child + return None + +def _add_entry_cpuprofiler_program(newtime, cpuprofiler): + curnode = _find_child(cpuprofiler['head']['children'], '(program)') + if cpuprofiler['lastTime'] != None: + lastTime = cpuprofiler['lastTime'] + while lastTime < newtime: + curnode['hitCount'] += 1 + cpuprofiler['samples'].append(curnode['callUID']) + cpuprofiler['timestamps'].append(int(lastTime*SECONDS_TO_NANOSECONDS)) + lastTime += SAMPLE_DELTA_IN_SECONDS + cpuprofiler['lastTime'] = lastTime + else: + cpuprofiler['lastTime'] = newtime + + +def _add_entry_cpuprofiler(stack, newtime, cpuprofiler): + index = len(stack) - 1 + marker = stack[index] + + if marker.name not in cpuprofiler['markers']: + cpuprofiler['markers'][marker.name] = cpuprofiler['id'] + cpuprofiler['callUID'] += 1 + callUID = cpuprofiler['markers'][marker.name] + + curnode = cpuprofiler['head'] + index = 0 + while index < len(stack): + newnode = _find_child(curnode['children'], stack[index].name) + if newnode == None: + newnode = {} + newnode['callUID'] = callUID + newnode['url'] = marker.url + newnode['functionName'] = stack[index].name + newnode['hitCount'] = 0 + newnode['lineNumber'] = marker.line + newnode['columnNumber'] = marker.col + newnode['scriptId'] = callUID + newnode['positionTicks'] = [] + newnode['id'] = cpuprofiler['id'] + cpuprofiler['id'] += 1 + newnode['children'] = [] + curnode['children'].append(newnode) + curnode['deoptReason'] = '' + curnode = newnode + index += 1 + + if cpuprofiler['lastTime'] == None: + cpuprofiler['lastTime'] = newtime + + if cpuprofiler['lastTime'] != None: + lastTime = cpuprofiler['lastTime'] + while lastTime < newtime: + curnode['hitCount'] += 1 + if len(curnode['positionTicks']) == 0: + ticks = {} + ticks['line'] = curnode['callUID'] + ticks['ticks'] = 0 + curnode['positionTicks'].append(ticks) + curnode['positionTicks'][0]['ticks'] += 1 + cpuprofiler['samples'].append(curnode['callUID']) + cpuprofiler['timestamps'].append(int(lastTime*1000*1000)) + lastTime += 0.0001 + cpuprofiler['lastTime'] = lastTime + +def _create_default_cpuprofiler_node(name, _id, _uid): + return {'functionName': name, + 'scriptId':'0', + 'url':'', + 'lineNumber':0, + 'columnNumber':0, + 'positionTicks':[], + 'id':_id, + 'callUID':_uid, + 'children': [], + 'hitCount': 0, + 'deoptReason':''} + +def main(): + parser = argparse.ArgumentParser(description="Converts JSON profile format to fbsystrace text output") + + parser.add_argument( + "-o", + dest = "output_file", + default = None, + help = "Output file for trace data") + parser.add_argument( + "-cpuprofiler", + dest = "output_cpuprofiler", + default = None, + help = "Output file for cpuprofiler data") + parser.add_argument( + "-map", + dest = "map_file", + default = None, + help = "Map file for symbolicating") + parser.add_argument( "file", help = "JSON trace input_file") + + args = parser.parse_args() + + markers = [] + with open(args.file, "r") as trace_file: + trace = json.load(trace_file) + for root_entry in trace["rootNodes"]: + _compute_markers(markers, root_entry, 0) + + mapcache = {} + for m in markers: + _calcurl(mapcache, m, args.map_file) + + sorted_markers = list(sorted(markers)); + + if args.output_cpuprofiler != None: + cpuprofiler = {} + cpuprofiler['startTime'] = None + cpuprofiler['endTime'] = None + cpuprofiler['lastTime'] = None + cpuprofiler['id'] = 4 + cpuprofiler['callUID'] = 4 + cpuprofiler['samples'] = [] + cpuprofiler['timestamps'] = [] + cpuprofiler['markers'] = {} + cpuprofiler['head'] = _create_default_cpuprofiler_node('(root)', 1, 1) + cpuprofiler['head']['children'].append(_create_default_cpuprofiler_node('(root)', 2, 2)) + cpuprofiler['head']['children'].append(_create_default_cpuprofiler_node('(program)', 3, 3)) + marker_stack = [] + with open(args.output_cpuprofiler, 'w') as file_out: + for marker in sorted_markers: + if len(marker_stack): + _add_entry_cpuprofiler(marker_stack, marker.timestamp, cpuprofiler) + else: + _add_entry_cpuprofiler_program(marker.timestamp, cpuprofiler) + if marker.is_end: + marker_stack.pop() + else: + marker_stack.append(marker) + cpuprofiler['startTime'] = cpuprofiler['timestamps'][0] / 1000000.0 + cpuprofiler['endTime'] = cpuprofiler['timestamps'][len(cpuprofiler['timestamps']) - 1] / 1000000.0 + json.dump(cpuprofiler, file_out, sort_keys=False, indent=4, separators=(',', ': ')) + + + if args.output_file != None: + with open(args.output_file,"w") as trace_file: + for marker in sorted_markers: + start_or_end = None + if marker.is_end: + start_or_end = "E" + else: + start_or_end = "B" + #output with timestamp at high level of precision + trace_file.write("json-0 [000] .... {0:.12f}: tracing_mark_write: {1}|0|{2}\n".format( + marker.timestamp, + start_or_end, + marker.name)) + +main() diff --git a/JSCLegacyProfiler/smap.py b/JSCLegacyProfiler/smap.py new file mode 100644 index 000000000..6c8f8e4ad --- /dev/null +++ b/JSCLegacyProfiler/smap.py @@ -0,0 +1,343 @@ + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + + +""" + adapted from https://github.com/martine/python-sourcemap into a reuasable module +""" + +""" + + Apache License + Version 2.0, January 2010 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + """ + +"""A module for parsing source maps, as output by the Closure and +CoffeeScript compilers and consumed by browsers. See + http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/ +""" + +import collections +import json +import sys +import bisect + +class entry(object): + def __init__(self, dst_line, dst_col, src, src_line, src_col): + self.dst_line = dst_line + self.dst_col = dst_col + self.src = src + self.src_line = src_line + self.src_col = src_col + + def __cmp__(self, other): + #print(self) + #print(other) + if self.dst_line < other.dst_line: + return -1 + if self.dst_line > other.dst_line: + return 1 + if self.dst_col < other.dst_col: + return -1 + if self.dst_col > other.dst_col: + return 1 + return 0 + +SmapState = collections.namedtuple( + 'SmapState', ['dst_line', 'dst_col', + 'src', 'src_line', 'src_col', + 'name']) + +# Mapping of base64 letter -> integer value. +B64 = dict((c, i) for i, c in + enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + '0123456789+/')) + + +def _parse_vlq(segment): + """Parse a string of VLQ-encoded data. + + Returns: + a list of integers. + """ + + values = [] + + cur, shift = 0, 0 + for c in segment: + val = B64[c] + # Each character is 6 bits: + # 5 of value and the high bit is the continuation. + val, cont = val & 0b11111, val >> 5 + cur += val << shift + shift += 5 + + if not cont: + # The low bit of the unpacked value is the sign. + cur, sign = cur >> 1, cur & 1 + if sign: + cur = -cur + values.append(cur) + cur, shift = 0, 0 + + if cur or shift: + raise Exception('leftover cur/shift in vlq decode') + + return values + + +def _parse_smap(file): + """Given a file-like object, yield SmapState()s as they are read from it.""" + + smap = json.load(file) + sources = smap['sources'] + names = smap['names'] + mappings = smap['mappings'] + lines = mappings.split(';') + + dst_col, src_id, src_line, src_col, name_id = 0, 0, 0, 0, 0 + for dst_line, line in enumerate(lines): + segments = line.split(',') + dst_col = 0 + for segment in segments: + if not segment: + continue + parsed = _parse_vlq(segment) + dst_col += parsed[0] + + src = None + name = None + if len(parsed) > 1: + src_id += parsed[1] + src = sources[src_id] + src_line += parsed[2] + src_col += parsed[3] + + if len(parsed) > 4: + name_id += parsed[4] + name = names[name_id] + + assert dst_line >= 0 + assert dst_col >= 0 + assert src_line >= 0 + assert src_col >= 0 + + yield SmapState(dst_line, dst_col, src, src_line, src_col, name) + +def find(entries, line, col): + test = entry(line, col, '', 0, 0) + index = bisect.bisect_right(entries, test) + if index == 0: + return None + return entries[index - 1] + +def parse(file): + # Simple demo that shows files that most contribute to total size. + lookup = [] + for state in _parse_smap(file): + lookup.append(entry(state.dst_line, state.dst_col, state.src, state.src_line, state.src_col)) + + sorted_lookup = list(sorted(lookup)) + return sorted_lookup diff --git a/JSCLegacyProfiler/trace_data.py b/JSCLegacyProfiler/trace_data.py new file mode 100644 index 000000000..7cda25adf --- /dev/null +++ b/JSCLegacyProfiler/trace_data.py @@ -0,0 +1,244 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import re +import unittest + +""" +# _-----=> irqs-off +# / _----=> need-resched +# | / _---=> hardirq/softirq +# || / _--=> preempt-depth +# ||| / delay +# TASK-PID CPU# |||| TIMESTAMP FUNCTION +# | | | |||| | | + -0 [001] ...2 3269.291072: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=mmcqd/0 next_pid=120 next_prio=120 +""" +TRACE_LINE_PATTERN = re.compile( + r'^\s*(?P.+)-(?P\d+)\s+(?:\((?P.+)\)\s+)?\[(?P\d+)\]\s+(?:(?P\S{4})\s+)?(?P[0-9.]+):\s+(?P.+)$') + +""" +Example lines from custom app traces: +0: B|27295|providerRemove +0: E +tracing_mark_write: S|27311|NNFColdStart|1112249168 +""" +APP_TRACE_LINE_PATTERN = re.compile( + r'^(?P.+?): (?P.+)$') + +""" +Example section names: +NNFColdStart +NNFColdStart<0> +NNFColdStart +NNFColdStart +""" +DECORATED_SECTION_NAME_PATTERN = re.compile(r'^(?P.*?)(?:<0>)?(?:<(?P.)(?P.*?)>)?$') + +SYSTRACE_LINE_TYPES = set(['0', 'tracing_mark_write']) + +class TraceLine(object): + def __init__(self, task, pid, tgid, cpu, flags, timestamp, function): + self.task = task + self.pid = pid + self.tgid = tgid + self.cpu = cpu + self.flags = flags + self.timestamp = timestamp + self.function = function + self.canceled = False + + @property + def is_app_trace_line(self): + return isinstance(self.function, AppTraceFunction) + + def cancel(self): + self.canceled = True + + def __str__(self): + if self.canceled: + return "" + elif self.tgid: + return "{task:>16s}-{pid:<5d} ({tgid:5s}) [{cpu:03d}] {flags:4s} {timestamp:12f}: {function}\n".format(**vars(self)) + elif self.flags: + return "{task:>16s}-{pid:<5d} [{cpu:03d}] {flags:4s} {timestamp:12f}: {function}\n".format(**vars(self)) + else: + return "{task:>16s}-{pid:<5d} [{cpu:03d}] {timestamp:12.6f}: {function}\n".format(**vars(self)) + + +class AppTraceFunction(object): + def __init__(self, type, args): + self.type = type + self.args = args + self.operation = args[0] + + if len(args) >= 2 and args[1]: + self.pid = int(args[1]) + if len(args) >= 3: + self._section_name, self.command, self.argument = _parse_section_name(args[2]) + args[2] = self._section_name + else: + self._section_name = None + self.command = None + self.argument = None + self.cookie = None + + @property + def section_name(self): + return self._section_name + + @section_name.setter + def section_name(self, value): + self._section_name = value + self.args[2] = value + + def __str__(self): + return "{type}: {args}".format(type=self.type, args='|'.join(self.args)) + + +class AsyncTraceFunction(AppTraceFunction): + def __init__(self, type, args): + super(AsyncTraceFunction, self).__init__(type, args) + + self.cookie = int(args[3]) + + +TRACE_TYPE_MAP = { + 'S': AsyncTraceFunction, + 'T': AsyncTraceFunction, + 'F': AsyncTraceFunction, +} + +def parse_line(line): + match = TRACE_LINE_PATTERN.match(line.strip()) + if not match: + return None + + task = match.group("task") + pid = int(match.group("pid")) + tgid = match.group("tgid") + cpu = int(match.group("cpu")) + flags = match.group("flags") + timestamp = float(match.group("timestamp")) + function = match.group("function") + + app_trace = _parse_function(function) + if app_trace: + function = app_trace + + return TraceLine(task, pid, tgid, cpu, flags, timestamp, function) + +def parse_dextr_line(line): + task = line["name"] + pid = line["pid"] + tgid = line["tid"] + cpu = None + flags = None + timestamp = line["ts"] + function = AppTraceFunction("DextrTrace", [line["ph"], line["pid"], line["name"]]) + + return TraceLine(task, pid, tgid, cpu, flags, timestamp, function) + + +def _parse_function(function): + line_match = APP_TRACE_LINE_PATTERN.match(function) + if not line_match: + return None + + type = line_match.group("type") + if not type in SYSTRACE_LINE_TYPES: + return None + + args = line_match.group("args").split('|') + if len(args) == 1 and len(args[0]) == 0: + args = None + + constructor = TRACE_TYPE_MAP.get(args[0], AppTraceFunction) + return constructor(type, args) + + +def _parse_section_name(section_name): + if section_name is None: + return section_name, None, None + + section_name_match = DECORATED_SECTION_NAME_PATTERN.match(section_name) + section_name = section_name_match.group("section_name") + command = section_name_match.group("command") + argument = section_name_match.group("argument") + return section_name, command, argument + + +def _format_section_name(section_name, command, argument): + if not command: + return section_name + + return "{section_name}<{command}{argument}>".format(**vars()) + + +class RoundTripFormattingTests(unittest.TestCase): + def testPlainSectionName(self): + section_name = "SectionName12345-5562342fas" + + self.assertEqual(section_name, _format_section_name(*_parse_section_name(section_name))) + + def testDecoratedSectionName(self): + section_name = "SectionName12345-5562342fas" + + self.assertEqual(section_name, _format_section_name(*_parse_section_name(section_name))) + + def testSimpleFunction(self): + function = "0: E" + + self.assertEqual(function, str(_parse_function(function))) + + def testFunctionWithoutCookie(self): + function = "0: B|27295|providerRemove" + + self.assertEqual(function, str(_parse_function(function))) + + def testFunctionWithCookie(self): + function = "0: S|27311|NNFColdStart|1112249168" + + self.assertEqual(function, str(_parse_function(function))) + + def testFunctionWithCookieAndArgs(self): + function = "0: T|27311|NNFColdStart|1122|Start" + + self.assertEqual(function, str(_parse_function(function))) + + def testFunctionWithArgsButNoPid(self): + function = "0: E|||foo=bar" + + self.assertEqual(function, str(_parse_function(function))) + + def testKitKatFunction(self): + function = "tracing_mark_write: B|14127|Looper.dispatchMessage|arg=>>>>> Dispatching to Handler (android.os.Handler) {422ae980} null: 0|Java" + + self.assertEqual(function, str(_parse_function(function))) + + def testNonSysTraceFunctionIgnored(self): + function = "sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=mmcqd/0 next_pid=120 next_prio=120" + + self.assertEqual(None, _parse_function(function)) + + def testLineWithFlagsAndTGID(self): + line = " -0 ( 550) [000] d..2 7953.258473: cpu_idle: state=1 cpu_id=0\n" + + self.assertEqual(line, str(parse_line(line))) + + def testLineWithFlagsAndNoTGID(self): + line = " -0 (-----) [000] d..2 7953.258473: cpu_idle: state=1 cpu_id=0\n" + + self.assertEqual(line, str(parse_line(line))) + + def testLineWithFlags(self): + line = " -0 [001] ...2 3269.291072: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=mmcqd/0 next_pid=120 next_prio=120\n" + + self.assertEqual(line, str(parse_line(line))) + + def testLineWithoutFlags(self): + line = " -0 [001] 3269.291072: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=mmcqd/0 next_pid=120 next_prio=120\n" + + self.assertEqual(line, str(parse_line(line))) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 482e213fc..19628f265 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -125,7 +125,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); [weakSelf stopLoadingWithError:error]; }); } - + sourceCode = source; dispatch_group_leave(initModulesAndLoadSource); }]; @@ -348,20 +348,20 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); - (void)stopLoadingWithError:(NSError *)error { RCTAssertMainThread(); - + if (!self.isValid || !self.loading) { return; } - + _loading = NO; - + NSArray *stack = error.userInfo[@"stack"]; if (stack) { [self.redBox showErrorMessage:error.localizedDescription withStack:stack]; } else { [self.redBox showError:error]; } - + NSDictionary *userInfo = @{@"bridge": self, @"error": error}; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification object:_parentBridge @@ -853,30 +853,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ NSString *log = RCTProfileEnd(self); - NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", self.bundleURL.scheme, self.bundleURL.host, self.bundleURL.port]; - NSURL *URL = [NSURL URLWithString:URLString]; - NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; - URLRequest.HTTPMethod = @"POST"; - [URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - NSURLSessionTask *task = - [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest - fromData:[log dataUsingEncoding:NSUTF8StringEncoding] - completionHandler: - ^(__unused NSData *data, __unused NSURLResponse *response, NSError *error) { - if (error) { - RCTLogError(@"%@", error.localizedDescription); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - [[[UIAlertView alloc] initWithTitle:@"Profile" - message:@"The profile has been generated, check the dev server log for instructions." - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil] show]; - }); - } - }]; - - [task resume]; + NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding]; + RCTProfileSendResult(self, @"systrace", logData); }]; } diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h index 794f4a910..eb8045fb6 100644 --- a/React/Base/RCTProfile.h +++ b/React/Base/RCTProfile.h @@ -119,6 +119,12 @@ RCT_EXTERN void RCTProfileHookModules(RCTBridge *); */ RCT_EXTERN void RCTProfileUnhookModules(RCTBridge *); +/** + * Send systrace or cpu profiling information to the packager + * to present to the user + */ +RCT_EXTERN void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *profielData); + #else #define RCTProfileBeginFlowEvent() @@ -144,4 +150,6 @@ RCT_EXTERN void RCTProfileUnhookModules(RCTBridge *); #define RCTProfileHookModules(...) #define RCTProfileUnhookModules(...) +#define RCTProfileSendResult(...) + #endif diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m index 86aab5652..f905c0c8c 100644 --- a/React/Base/RCTProfile.m +++ b/React/Base/RCTProfile.m @@ -18,6 +18,7 @@ #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTDefines.h" +#import "RCTLog.h" #import "RCTModuleData.h" #import "RCTUtils.h" @@ -402,4 +403,44 @@ void _RCTProfileEndFlowEvent(NSNumber *flowID) ); } +void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *data) +{ + if (![bridge.bundleURL.scheme hasPrefix:@"http"]) { + RCTLogError(@"Cannot update profile information"); + return; + } + + NSURL *URL = [NSURL URLWithString:[@"/" stringByAppendingString:route] relativeToURL:bridge.bundleURL]; + + NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; + URLRequest.HTTPMethod = @"POST"; + [URLRequest setValue:@"application/json" + forHTTPHeaderField:@"Content-Type"]; + + NSURLSessionTask *task = + [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest + fromData:data + completionHandler: + ^(NSData *responseData, __unused NSURLResponse *response, NSError *error) { + if (error) { + RCTLogError(@"%@", error.localizedDescription); + } else { + NSString *message = [[NSString alloc] initWithData:responseData + encoding:NSUTF8StringEncoding]; + + if (message.length) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[[UIAlertView alloc] initWithTitle:@"Profile" + message:message + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil] show]; + }); + } + } + }]; + + [task resume]; +} + #endif diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 60cdd3cd1..44be17af2 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -229,7 +229,11 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) if (isProfiling) { NSString *outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cpu_profile.json"]; nativeProfilerEnd(context, "profile", outputFile.UTF8String); - RCTLogInfo(@"CPU profile outputed to '%@'", outputFile); + NSData *profileData = [NSData dataWithContentsOfFile:outputFile + options:NSDataReadingMappedIfSafe + error:NULL]; + + RCTProfileSendResult(bridge, @"cpu-profile", profileData); } else { nativeProfilerStart(context, "profile"); } diff --git a/packager/packager.js b/packager/packager.js index 2a95dfc91..a543aba02 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -224,7 +224,7 @@ function statusPageMiddleware(req, res, next) { } function systraceProfileMiddleware(req, res, next) { - if (req.url !== '/profile') { + if (req.url !== '/systrace') { next(); return; } @@ -237,33 +237,54 @@ function systraceProfileMiddleware(req, res, next) { childProcess.exec(cmd, function(error) { if (error) { if (error.code === 127) { - console.error( + res.end( '\n** Failed executing `' + cmd + '` **\n\n' + - 'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' + - 'You can get it at:\n\n' + - ' https://github.com/google/trace-viewer\n\n' + - 'If it\'s not in your path, you can set a custom path with:\n\n' + - ' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + - 'NOTE: Your profile data was kept at:\n\n' + - ' ' + dumpName + 'Google trace-viewer is required to visualize the data, You can install it with `brew install trace2html`\n\n' + + 'NOTE: Your profile data was kept at:\n' + dumpName ); } else { - console.error('Unknown error', error); + console.error(error); + res.end('Unknown error %s', error.message); } - res.end(); return; } else { childProcess.exec('rm ' + dumpName); childProcess.exec('open ' + dumpName.replace(/json$/, 'html'), function(err) { if (err) { console.error(err); + res.end(err.message); + } else { + res.end(); } - res.end(); }); } }); } +function cpuProfileMiddleware(req, res, next) { + if (req.url !== '/cpu-profile') { + next(); + return; + } + + console.log('Dumping CPU profile information...'); + const dumpName = '/tmp/cpu-profile_' + Date.now(); + fs.writeFileSync(dumpName + '.json', req.rawBody); + + const cmd = path.join(__dirname, '..', 'JSCLegacyProfiler', 'json2trace') + ' -cpuprofiler ' + dumpName + '.cpuprofile ' + dumpName + '.json'; + childProcess.exec(cmd, function(error) { + if (error) { + console.error(error); + res.end('Unknown error: %s', error.message); + } else { + res.end( + 'Your profile was generated at\n\n' + dumpName + '.cpuprofile\n\n' + + 'Open `Chrome Dev Tools > Profiles > Load` and select the profile to visualize it.' + ); + } + }); +} + function getAppMiddleware(options) { var transformerPath = options.transformer; if (!isAbsolutePath(transformerPath)) { @@ -297,6 +318,7 @@ function runServer( .use(getDevToolsLauncher(options)) .use(statusPageMiddleware) .use(systraceProfileMiddleware) + .use(cpuProfileMiddleware) // Temporarily disable flow check until it's more stable //.use(getFlowTypeCheckMiddleware(options)) .use(getAppMiddleware(options));