summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/catapult/tracing/tracing/ui
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/ui')
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html181
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html147
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html266
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html35
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html207
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html141
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html200
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html351
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html177
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html52
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html267
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html347
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html222
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html893
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html1261
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html178
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html354
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html451
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html4045
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html149
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html101
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html774
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html840
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html593
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html915
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html1241
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html382
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html496
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html47
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html211
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html207
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html358
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html112
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html46
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html77
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html234
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html104
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html52
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html80
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html135
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html137
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html354
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html97
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html75
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html149
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html80
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html356
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html277
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html53
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html142
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html80
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html186
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html81
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html38
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html195
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html205
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html64
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html69
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/animation.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html144
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html166
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html253
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html195
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/base.html18
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html135
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/camera.html350
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html59
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html453
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html571
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html124
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html109
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html135
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html427
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html276
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/constants.html20
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html95
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/d3.html9
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js11
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html390
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html169
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html185
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html128
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html415
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html103
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html81
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html147
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html27
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/file.html36
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html229
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html258
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html55
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/heading.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html310
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html139
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html47
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html98
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html180
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html183
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html41
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html577
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html98
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html117
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html127
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html119
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html351
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html118
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html13
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html16
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html688
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html150
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html273
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html160
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/table.html1808
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html2115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html327
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html78
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/ui.html172
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html100
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html246
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/utils.html83
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html77
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html280
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html317
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html204
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html122
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html26
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css25
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html216
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html396
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html88
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html372
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html187
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html57
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html689
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html426
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html115
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html14
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html451
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html134
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html60
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.pngbin0 -> 3344 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg114
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html336
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html142
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html1200
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html113
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html165
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html55
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html455
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html32
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html458
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html505
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html261
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html140
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html222
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html304
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html56
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html10
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.pngbin0 -> 245 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css15
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html48
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html229
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html33
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html123
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html65
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html185
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html463
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html19
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html21
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html172
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html61
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html347
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html165
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html334
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html148
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html12
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css15
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html451
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html116
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css28
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html84
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html18
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html728
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html198
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html181
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html87
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html197
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html236
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html42
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html114
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html17
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/find_control.html177
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html66
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/find_controller.html154
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html366
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.pngbin0 -> 14088 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.pngbin0 -> 382 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.pngbin0 -> 3264 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.pngbin0 -> 4097 bytes
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html132
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html196
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html200
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html22
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html221
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html36
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html222
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html50
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html284
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html98
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html117
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html175
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html85
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html40
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html249
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html1179
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html200
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html641
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html245
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html17
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html63
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html31
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html217
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html442
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html68
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html76
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html179
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html328
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html43
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html37
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html566
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html331
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html213
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html313
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html281
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html454
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html106
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html138
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html79
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html129
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html205
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html140
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html94
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html91
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html215
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html90
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html145
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css18
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html236
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html137
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html71
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html107
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html62
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html51
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html82
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html251
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html121
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html253
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html270
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html67
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html99
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html534
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html178
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html240
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html294
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html105
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html81
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html121
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html70
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html58
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html130
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html110
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html155
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css39
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html313
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css8
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html249
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html412
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html34
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html167
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html299
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html44
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html29
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css7
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html45
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html131
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css10
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html185
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html141
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css33
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html167
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html309
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html133
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html92
-rw-r--r--chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html67
410 files changed, 73569 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html
new file mode 100644
index 00000000000..b44741aace1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-a-alert-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-alert-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ this.$.table.tableColumns = [
+ {
+ title: 'Label',
+ value(row) { return row.name; },
+ width: '150px'
+ },
+ {
+ title: 'Value',
+ width: '100%',
+ value(row) { return row.value; }
+ }
+ ];
+ this.$.table.showHeader = false;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ getRowsForSingleAlert_(alert) {
+ const rows = [];
+
+ // Arguments
+ for (const argName in alert.args) {
+ const argView =
+ document.createElement('tr-ui-a-generic-object-view');
+ argView.object = alert.args[argName];
+ rows.push({ name: argName, value: argView });
+ }
+
+ // Associated events
+ if (alert.associatedEvents.length) {
+ alert.associatedEvents.forEach(function(event, i) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(
+ new tr.model.EventSet(event), event.title);
+
+ let valueString = '';
+ if (event instanceof tr.model.TimedEvent) {
+ valueString = 'took ' + event.duration.toFixed(2) + 'ms';
+ }
+
+ rows.push({
+ name: linkEl,
+ value: valueString
+ });
+ });
+ }
+
+ // Description
+ const descriptionEl = tr.ui.b.createDiv({
+ textContent: alert.info.description,
+ maxWidth: '300px'
+ });
+ rows.push({
+ name: 'Description',
+ value: descriptionEl
+ });
+
+ // Additional Reading Links
+ if (alert.info.docLinks) {
+ alert.info.docLinks.forEach(function(linkObject) {
+ const linkEl = document.createElement('a');
+ linkEl.target = '_blank';
+ linkEl.href = linkObject.href;
+ Polymer.dom(linkEl).textContent = Polymer.dom(linkObject).textContent;
+ rows.push({
+ name: linkObject.label,
+ value: linkEl
+ });
+ });
+ }
+ return rows;
+ },
+
+ getRowsForAlerts_(alerts) {
+ if (alerts.length === 1) {
+ const rows = [{
+ name: 'Alert',
+ value: tr.b.getOnlyElement(alerts).title
+ }];
+ const detailRows = this.getRowsForSingleAlert_(tr.b.getOnlyElement(
+ alerts));
+ rows.push.apply(rows, detailRows);
+ return rows;
+ }
+ return alerts.map(function(alert) {
+ return {
+ name: 'Alert',
+ value: alert.title,
+ isExpanded: alerts.size < 10, // This is somewhat arbitrary for now.
+ subRows: this.getRowsForSingleAlert_(alert)
+ };
+ }, this);
+ },
+
+ updateContents_() {
+ if (this.currentSelection_ === undefined) {
+ this.$.table.rows = [];
+ this.$.table.rebuild();
+ return;
+ }
+
+ const alerts = this.currentSelection_;
+ this.$.table.tableRows = this.getRowsForAlerts_(alerts);
+ this.$.table.rebuild();
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+ const result = new tr.model.EventSet();
+ for (const event of this.currentSelection_) {
+ result.addEventSet(event.associatedEvents);
+ }
+ return result;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-alert-sub-view',
+ tr.model.Alert,
+ {
+ multi: false,
+ title: 'Alert',
+ });
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-alert-sub-view',
+ tr.model.Alert,
+ {
+ multi: true,
+ title: 'Alerts',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html
new file mode 100644
index 00000000000..574cf5f0b86
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/alert_sub_view_test.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('instantiate', function() {
+ const slice = newSliceEx({title: 'b', start: 0, duration: 0.002});
+
+ const alertInfo = new tr.model.EventInfo(
+ 'alertInfo', 'Critical alert',
+ [{
+ label: 'Project Page',
+ textContent: 'Trace-Viewer Github Project',
+ href: 'https://github.com/google/trace-viewer/'
+ }]);
+
+ const alert = new tr.model.Alert(alertInfo, 5, [slice]);
+ assert.strictEqual(1, alert.associatedEvents.length);
+
+ const subView = document.createElement('tr-ui-a-alert-sub-view');
+ subView.selection = new tr.model.EventSet(alert);
+ assert.isTrue(
+ subView.relatedEventsToHighlight.equals(alert.associatedEvents));
+ this.addHTMLOutput(subView);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ subView, 'tr-ui-b-table');
+
+ const rows = table.tableRows;
+ const columns = table.tableColumns;
+ assert.lengthOf(rows, 4);
+ assert.lengthOf(columns, 2);
+ });
+
+ test('instantiate_twoAlertsWithRelatedEvents', function() {
+ const slice1 = newSliceEx({title: 'b', start: 0, duration: 0.002});
+ const slice2 = newSliceEx({title: 'b', start: 1, duration: 0.002});
+
+ const alertInfo1 = new tr.model.EventInfo(
+ 'alertInfo1', 'Critical alert',
+ [{
+ label: 'Project Page',
+ textContent: 'Trace-Viewer Github Project',
+ href: 'https://github.com/google/trace-viewer/'
+ }]);
+
+ const alertInfo2 = new tr.model.EventInfo(
+ 'alertInfo2', 'Critical alert',
+ [{
+ label: 'Google Homepage',
+ textContent: 'Google Search Page',
+ href: 'http://www.google.com'
+ }]);
+
+ const alert1 = new tr.model.Alert(alertInfo1, 5, [slice1]);
+ const alert2 = new tr.model.Alert(alertInfo2, 5, [slice2]);
+
+ const subView = document.createElement('tr-ui-a-alert-sub-view');
+ subView.selection = new tr.model.EventSet([alert1, alert2]);
+ assert.isTrue(subView.relatedEventsToHighlight.equals(
+ new tr.model.EventSet([
+ tr.b.getOnlyElement(alert1.associatedEvents),
+ tr.b.getOnlyElement(alert2.associatedEvents)
+ ])));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html
new file mode 100644
index 00000000000..8d996afeeb9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<dom-module id='tr-ui-a-analysis-link'>
+ <template>
+ <style>
+ :host {
+ display: inline;
+ cursor: pointer;
+ cursor: pointer;
+ white-space: nowrap;
+ }
+ a {
+ text-decoration: underline;
+ }
+ </style>
+ <a href="{{href}}" on-click="onClicked_" on-mouseenter="onMouseEnter_" on-mouseleave="onMouseLeave_"><slot></slot></a>
+
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-analysis-link',
+
+ properties: {
+ href: {
+ type: String
+ }
+ },
+
+ listeners: {
+ 'click': 'onClicked_',
+ 'mouseenter': 'onMouseEnter_',
+ 'mouseleave': 'onMouseLeave_'
+ },
+
+ ready() {
+ this.selection_ = undefined;
+ },
+
+ attached() {
+ // Save an instance of the controller since it's going to be used in
+ // detached() where it can no longer be obtained.
+ this.controller_ =
+ tr.c.BrushingStateController.getControllerForElement(this);
+ },
+
+ detached() {
+ // Reset highlights.
+ this.clearHighlight_();
+ this.controller_ = undefined;
+ },
+
+ set color(c) {
+ this.style.color = c;
+ },
+
+ /**
+ * @return {*|function():*}
+ */
+ get selection() {
+ return this.selection_;
+ },
+
+ /**
+ * |selection| can be anything except a function, or else a function that
+ * can return anything.
+ *
+ * In the context of trace_viewer, |selection| is typically an EventSet,
+ * whose events will be highlighted by trace_viewer when this link is
+ * clicked or mouse-entered.
+ *
+ * If |selection| is not a function, then it will be dispatched to this
+ * link's embedder via a RequestSelectionChangeEvent when this link is
+ * clicked or mouse-entered.
+ *
+ * If |selection| is a function, then it will be called when this link is
+ * clicked or mouse-entered, and its result will be dispatched to this
+ * link's embedder via a RequestSelectionChangeEvent.
+ *
+ * @param {*|function():*} selection
+ */
+ set selection(selection) {
+ this.selection_ = selection;
+ Polymer.dom(this).textContent = selection.userFriendlyName;
+ },
+
+ setSelectionAndContent(selection, opt_textContent) {
+ this.selection_ = selection;
+ if (opt_textContent) {
+ Polymer.dom(this).textContent = opt_textContent;
+ }
+ },
+
+ /**
+ * If |selection| is a function, call it and return the result.
+ * Otherwise return |selection| directly.
+ *
+ * @return {*}
+ */
+ getCurrentSelection_() {
+ // Gets the current selection, invoking the selection function if needed.
+ if (typeof this.selection_ === 'function') {
+ return this.selection_();
+ }
+ return this.selection_;
+ },
+
+ setHighlight_(opt_eventSet) {
+ if (this.controller_) {
+ this.controller_.changeAnalysisLinkHoveredEvents(opt_eventSet);
+ }
+ },
+
+ clearHighlight_(opt_eventSet) {
+ this.setHighlight_();
+ },
+
+ onClicked_(clickEvent) {
+ if (!this.selection_) return;
+
+ clickEvent.stopPropagation();
+
+ const event = new tr.model.RequestSelectionChangeEvent();
+ event.selection = this.getCurrentSelection_();
+ this.dispatchEvent(event);
+ },
+
+ onMouseEnter_() {
+ this.setHighlight_(this.getCurrentSelection_());
+ },
+
+ onMouseLeave_() {
+ this.clearHighlight_();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html
new file mode 100644
index 00000000000..e8caed8f601
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_link_test.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('testBasic', function() {
+ const link = document.createElement('tr-ui-a-analysis-link');
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ link.selection = new tr.model.EventSet(s10);
+ this.addHTMLOutput(link);
+
+ let didRSC = false;
+ link.addEventListener('requestSelectionChange', function(e) {
+ didRSC = true;
+ assert.isTrue(e.selection.equals(new tr.model.EventSet(s10)));
+ });
+ link.click();
+ assert.isTrue(didRSC);
+ });
+
+ test('testGeneratorVersion', function() {
+ const link = document.createElement('tr-ui-a-analysis-link');
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ function selectionGenerator() {
+ return new tr.model.EventSet(s10);
+ }
+ selectionGenerator.userFriendlyName = 'hello world';
+ link.selection = selectionGenerator;
+ this.addHTMLOutput(link);
+
+ let didRSC = false;
+ link.addEventListener('requestSelectionChange', function(e) {
+ assert.isTrue(e.selection.equals(new tr.model.EventSet(s10)));
+ didRSC = true;
+ });
+ link.click();
+ assert.isTrue(didRSC);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html
new file mode 100644
index 00000000000..8bd967c8c75
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view.html
@@ -0,0 +1,266 @@
+<!DOCTYPE html>
+<!--
+Copyright 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<!--
+@fileoverview Polymer element for various analysis sub-views.
+-->
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const AnalysisSubView = {
+ set tabLabel(label) {
+ Polymer.dom(this).setAttribute('tab-label', label);
+ },
+
+ get tabLabel() {
+ return this.getAttribute('tab-label');
+ },
+
+ get requiresTallView() {
+ return false;
+ },
+
+ get relatedEventsToHighlight() {
+ return undefined;
+ },
+
+ /**
+ * Each element extending this one must implement
+ * a 'selection' property.
+ */
+ set selection(selection) {
+ throw new Error('Not implemented!');
+ },
+
+ get selection() {
+ throw new Error('Not implemented!');
+ }
+ };
+
+ // Basic registry.
+ const allTypeInfosByEventProto = new Map();
+ let onlyRootTypeInfosByEventProto = undefined;
+ let eventProtoToRootTypeInfoMap = undefined;
+
+ function AnalysisSubViewTypeInfo(eventConstructor, options) {
+ if (options.multi === undefined) {
+ throw new Error('missing field: multi');
+ }
+ if (options.title === undefined) {
+ throw new Error('missing field: title');
+ }
+ this.eventConstructor = eventConstructor;
+
+ this.singleTagName = undefined;
+ this.singleTitle = undefined;
+
+ this.multiTagName = undefined;
+ this.multiTitle = undefined;
+
+ // This is computed by rebuildRootSubViewTypeInfos, so don't muck with it!
+ this.childrenTypeInfos_ = undefined;
+ }
+
+ AnalysisSubViewTypeInfo.prototype = {
+ get childrenTypeInfos() {
+ return this.childrenTypeInfos_;
+ },
+
+ resetchildrenTypeInfos() {
+ this.childrenTypeInfos_ = [];
+ }
+ };
+
+ AnalysisSubView.register = function(tagName, eventConstructor, options) {
+ let typeInfo = allTypeInfosByEventProto.get(eventConstructor.prototype);
+ if (typeInfo === undefined) {
+ typeInfo = new AnalysisSubViewTypeInfo(eventConstructor, options);
+ allTypeInfosByEventProto.set(typeInfo.eventConstructor.prototype,
+ typeInfo);
+
+ onlyRootTypeInfosByEventProto = undefined;
+ }
+
+ if (!options.multi) {
+ if (typeInfo.singleTagName !== undefined) {
+ throw new Error('SingleTagName already set');
+ }
+ typeInfo.singleTagName = tagName;
+ typeInfo.singleTitle = options.title;
+ } else {
+ if (typeInfo.multiTagName !== undefined) {
+ throw new Error('MultiTagName already set');
+ }
+ typeInfo.multiTagName = tagName;
+ typeInfo.multiTitle = options.title;
+ }
+ return typeInfo;
+ };
+
+ function rebuildRootSubViewTypeInfos() {
+ onlyRootTypeInfosByEventProto = new Map();
+ allTypeInfosByEventProto.forEach(function(typeInfo) {
+ typeInfo.resetchildrenTypeInfos();
+ });
+
+ // Find all root typeInfos.
+ allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) {
+ const eventPrototype = typeInfo.eventConstructor.prototype;
+
+ let lastEventProto = eventPrototype;
+ let curEventProto = eventPrototype.__proto__;
+ while (true) {
+ if (!allTypeInfosByEventProto.has(curEventProto)) {
+ const rootTypeInfo = allTypeInfosByEventProto.get(lastEventProto);
+ const rootEventProto = lastEventProto;
+
+ const isNew = onlyRootTypeInfosByEventProto.has(rootEventProto);
+ onlyRootTypeInfosByEventProto.set(rootEventProto,
+ rootTypeInfo);
+ break;
+ }
+
+ lastEventProto = curEventProto;
+ curEventProto = curEventProto.__proto__;
+ }
+ });
+
+ // Build the childrenTypeInfos array.
+ allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) {
+ const eventPrototype = typeInfo.eventConstructor.prototype;
+ const parentEventProto = eventPrototype.__proto__;
+ const parentTypeInfo = allTypeInfosByEventProto.get(parentEventProto);
+ if (!parentTypeInfo) return;
+ parentTypeInfo.childrenTypeInfos.push(typeInfo);
+ });
+
+ // Build the eventProto to rootTypeInfo map.
+ eventProtoToRootTypeInfoMap = new Map();
+ allTypeInfosByEventProto.forEach(function(typeInfo, eventProto) {
+ const eventPrototype = typeInfo.eventConstructor.prototype;
+
+ let curEventProto = eventPrototype;
+ while (true) {
+ if (onlyRootTypeInfosByEventProto.has(curEventProto)) {
+ const rootTypeInfo = onlyRootTypeInfosByEventProto.get(
+ curEventProto);
+ eventProtoToRootTypeInfoMap.set(eventPrototype,
+ rootTypeInfo);
+ break;
+ }
+ curEventProto = curEventProto.__proto__;
+ }
+ });
+ }
+
+ function findLowestTypeInfoForEvents(thisTypeInfo, events) {
+ if (events.length === 0) return thisTypeInfo;
+ const event0 = tr.b.getFirstElement(events);
+
+ let candidateSubTypeInfo;
+ for (let i = 0; i < thisTypeInfo.childrenTypeInfos.length; i++) {
+ const childTypeInfo = thisTypeInfo.childrenTypeInfos[i];
+ if (event0 instanceof childTypeInfo.eventConstructor) {
+ candidateSubTypeInfo = childTypeInfo;
+ break;
+ }
+ }
+ if (!candidateSubTypeInfo) return thisTypeInfo;
+
+ // Validate that all the other events are instances of the candidate type.
+ let allMatch = true;
+ for (const event of events) {
+ if (event instanceof candidateSubTypeInfo.eventConstructor) continue;
+ allMatch = false;
+ break;
+ }
+
+ if (!allMatch) {
+ return thisTypeInfo;
+ }
+
+ return findLowestTypeInfoForEvents(candidateSubTypeInfo, events);
+ }
+
+ const primaryEventProtoToTypeInfoMap = new Map();
+ function getRootTypeInfoForEvent(event) {
+ const curProto = event.__proto__;
+ const typeInfo = primaryEventProtoToTypeInfoMap.get(curProto);
+ if (typeInfo) return typeInfo;
+ return getRootTypeInfoForEventSlow(event);
+ }
+
+ function getRootTypeInfoForEventSlow(event) {
+ let typeInfo;
+ let curProto = event.__proto__;
+ while (true) {
+ if (curProto === Object.prototype) {
+ throw new Error('No view registered for ' + event.toString());
+ }
+ typeInfo = onlyRootTypeInfosByEventProto.get(curProto);
+ if (typeInfo) {
+ primaryEventProtoToTypeInfoMap.set(event.__proto__, typeInfo);
+ return typeInfo;
+ }
+ curProto = curProto.__proto__;
+ }
+ }
+
+ AnalysisSubView.getEventsOrganizedByTypeInfo = function(selection) {
+ if (onlyRootTypeInfosByEventProto === undefined) {
+ rebuildRootSubViewTypeInfos();
+ }
+
+ // Base grouping.
+ const eventsByRootTypeInfo = tr.b.groupIntoMap(
+ selection,
+ function(event) {
+ return getRootTypeInfoForEvent(event);
+ },
+ this, tr.model.EventSet);
+
+ // Now, try to lower the typeinfo to the most specific type that still
+ // encompasses the event group.
+ //
+ // For instance, if we have 3 ThreadSlices, and all three are V8 slices,
+ // then we can convert this to use the V8Slices's typeinfos. But, if one
+ // of those slices was not a V8Slice, then we must still use
+ // ThreadSlice.
+ //
+ // The reason for this is for the confusion that might arise from the
+ // alternative. Suppose you click on a set of mixed slices, we want to show
+ // you the most correct information, and let you navigate to . If we instead
+ // showed you a V8 slices tab, and a Slices tab, we present the user with an
+ // ambiguity: is the V8 slice also in the Slices tab? Or is it not? Better,
+ // we think, to just only ever show an event in one place at a time, and
+ // avoid the possible confusion.
+ const eventsByLowestTypeInfo = new Map();
+ eventsByRootTypeInfo.forEach(function(events, typeInfo) {
+ const lowestTypeInfo = findLowestTypeInfoForEvents(typeInfo, events);
+ eventsByLowestTypeInfo.set(lowestTypeInfo, events);
+ });
+
+ return eventsByLowestTypeInfo;
+ };
+
+ return {
+ AnalysisSubView,
+ AnalysisSubViewTypeInfo,
+ };
+});
+
+// Dummy element for testing
+Polymer({
+ is: 'tr-ui-a-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView]
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html
new file mode 100644
index 00000000000..0f3e85ea4ec
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_sub_view_test.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('subViewThrowsNotImplementedErrors', function() {
+ const subView = document.createElement('tr-ui-a-sub-view');
+
+ assert.throw(function() {
+ subView.selection = new tr.model.EventSet();
+ }, 'Not implemented!');
+
+ assert.throw(function() {
+ const viewSelection = subView.selection;
+ }, 'Not implemented!');
+
+ subView.tabLabel = 'Tab Label';
+ assert.strictEqual(subView.getAttribute('tab-label'), 'Tab Label');
+ assert.strictEqual(subView.tabLabel, 'Tab Label');
+
+ subView.tabLabel = 'New Label';
+ assert.strictEqual(subView.getAttribute('tab-label'), 'New Label');
+ assert.strictEqual(subView.tabLabel, 'New Label');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html
new file mode 100644
index 00000000000..edc14edca11
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/container_memory_dump_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/counter_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_async_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_cpu_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_flow_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_frame_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/multi_instant_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_object_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_thread_slice_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/multi_thread_time_slice_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/multi_user_expectation_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_async_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_cpu_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_flow_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_frame_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_instant_event_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_object_instance_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_object_snapshot_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_power_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_sample_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_thread_slice_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_thread_time_slice_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/single_user_expectation_sub_view.html">
+<link rel="import" href="/tracing/ui/base/tab_view.html">
+
+<!--
+@fileoverview A component used to display an analysis of a selection,
+using custom elements specialized for different event types.
+-->
+<dom-module id='tr-ui-a-analysis-view'>
+ <template>
+ <style>
+ :host {
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ height: 275px;
+ overflow: auto;
+ }
+
+ :host(.tall-mode) {
+ height: 525px;
+ }
+ </style>
+ <slot></slot>
+ </template>
+</dom-module>
+<script>
+'use strict';
+(function() {
+ const EventRegistry = tr.model.EventRegistry;
+
+ /** Returns the label that goes next to the list of tabs. */
+ function getTabStripLabel(numEvents) {
+ if (numEvents === 0) {
+ return 'Nothing selected. Tap stuff.';
+ } else if (numEvents === 1) {
+ return '1 item selected.';
+ }
+ return numEvents + ' items selected.';
+ }
+
+ function createSubView(subViewTypeInfo, selection) {
+ let tagName;
+ if (selection.length === 1) {
+ tagName = subViewTypeInfo.singleTagName;
+ } else {
+ tagName = subViewTypeInfo.multiTagName;
+ }
+
+ if (tagName === undefined) {
+ throw new Error('No view registered for ' +
+ subViewTypeInfo.eventConstructor.name);
+ }
+ const subView = document.createElement(tagName);
+
+ let title;
+ if (selection.length === 1) {
+ title = subViewTypeInfo.singleTitle;
+ } else {
+ title = subViewTypeInfo.multiTitle;
+ }
+ title += ' (' + selection.length + ')';
+ subView.tabLabel = title;
+
+ subView.selection = selection;
+ return subView;
+ }
+
+ Polymer({
+ is: 'tr-ui-a-analysis-view',
+
+ ready() {
+ this.brushingStateController_ = undefined;
+ this.lastSelection_ = undefined;
+ this.tabView_ = document.createElement('tr-ui-b-tab-view');
+ this.tabView_.addEventListener(
+ 'selected-tab-change', this.onSelectedSubViewChanged_.bind(this));
+
+ Polymer.dom(this).appendChild(this.tabView_);
+ },
+
+ set tallMode(value) {
+ Polymer.dom(this).classList.toggle('tall-mode', value);
+ },
+
+ get tallMode() {
+ return Polymer.dom(this).classList.contains('tall-mode');
+ },
+
+ get tabView() {
+ return this.tabView_;
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ set brushingStateController(brushingStateController) {
+ if (this.brushingStateController_) {
+ this.brushingStateController_.removeEventListener(
+ 'change', this.onSelectionChanged_.bind(this));
+ }
+
+ this.brushingStateController_ = brushingStateController;
+ if (this.brushingStateController) {
+ this.brushingStateController_.addEventListener(
+ 'change', this.onSelectionChanged_.bind(this));
+ }
+
+ // The new brushing controller may have a different selection than the
+ // last one, so we have to refresh the subview.
+ this.onSelectionChanged_();
+ },
+
+ get selection() {
+ return this.brushingStateController_.selection;
+ },
+
+ onSelectionChanged_(e) {
+ if (this.lastSelection_ && this.selection.equals(this.lastSelection_)) {
+ return;
+ }
+ this.lastSelection_ = this.selection;
+
+ this.tallMode = false;
+
+ this.tabView_.label = getTabStripLabel(this.selection.length);
+ const eventsByBaseTypeName =
+ this.selection.getEventsOrganizedByBaseType(true);
+
+ const ASV = tr.ui.analysis.AnalysisSubView;
+ const eventsByTagName = ASV.getEventsOrganizedByTypeInfo(this.selection);
+ const newSubViews = [];
+ eventsByTagName.forEach(function(events, typeInfo) {
+ newSubViews.push(createSubView(typeInfo, events));
+ });
+
+ this.tabView_.resetSubViews(newSubViews);
+ },
+
+ onSelectedSubViewChanged_() {
+ const selectedSubView = this.tabView_.selectedSubView;
+
+ if (!selectedSubView) {
+ this.tallMode = false;
+ this.maybeChangeRelatedEvents_(undefined);
+ return;
+ }
+
+ this.tallMode = selectedSubView.requiresTallView;
+ this.maybeChangeRelatedEvents_(selectedSubView.relatedEventsToHighlight);
+ },
+
+ /** Changes the highlighted related events if possible. */
+ maybeChangeRelatedEvents_(events) {
+ if (this.brushingStateController) {
+ this.brushingStateController.changeAnalysisViewRelatedEvents(events);
+ }
+ }
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html
new file mode 100644
index 00000000000..fa7b51256a6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/analysis_view_test.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/counter_sample.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/user_model/stub_expectation.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const BrushingStateController = tr.c.BrushingStateController;
+ const Model = tr.Model;
+ const Counter = tr.model.Counter;
+ const CounterSeries = tr.model.CounterSeries;
+ const CounterSample = tr.model.CounterSample;
+ const newThreadSlice = tr.c.TestUtils.newThreadSlice;
+ const SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
+ const StubExpectation = tr.model.um.StubExpectation;
+
+ function assertEventSet(actualEventSet, expectedEvents) {
+ const expectedEventSet = new EventSet(expectedEvents);
+ assert.isTrue(actualEventSet.equals(expectedEventSet),
+ 'EventSet objects are not equal');
+ }
+
+ function checkTab(tab, expectedTagName, expectedSelectionEvents) {
+ assert.strictEqual(tab.tagName, expectedTagName.toUpperCase());
+ assertEventSet(tab.selection, expectedSelectionEvents);
+ }
+
+ test('selectedTabChange', function() {
+ // Set up the model.
+ const model = new Model();
+ const process = model.getOrCreateProcess(1);
+
+ const counter = process.getOrCreateCounter('universe', 'planets');
+ const series = counter.addSeries(new CounterSeries('x', 0));
+ const sample1 = series.addCounterSample(0, 100);
+ const sample2 = series.addCounterSample(1, 90);
+ const sample3 = series.addCounterSample(2, 80);
+
+ const thread = process.getOrCreateThread(2);
+ const slice1 = newThreadSlice(thread, SCHEDULING_STATE.RUNNING, 0, 1);
+ const slice2 = newThreadSlice(thread, SCHEDULING_STATE.SLEEPING, 1, 2.718);
+ thread.timeSlices = [slice1, slice2];
+
+ const record1 = new StubExpectation(
+ {parentModel: model, initiatorTitle: 'r1', start: 200, duration: 300});
+ record1.associatedEvents.push(sample1);
+ record1.associatedEvents.push(slice1);
+ const record2 = new StubExpectation(
+ {parentModel: model, initiatorTitle: 'r2', start: 600, duration: 100});
+ record2.associatedEvents.push(sample2);
+ record2.associatedEvents.push(sample3);
+ record2.associatedEvents.push(slice1);
+
+ // Set up the analysis views and brushing state controller.
+ const analysisView = document.createElement('tr-ui-a-analysis-view');
+ this.addHTMLOutput(analysisView);
+ const tabView = analysisView.tabView;
+ const controller = new BrushingStateController(undefined);
+ analysisView.brushingStateController = controller;
+
+ function checkSelectedTab(expectedSelectedTab, expectedRelatedEvents) {
+ assert.strictEqual(tabView.selectedSubView, expectedSelectedTab);
+ assertEventSet(controller.currentBrushingState.analysisViewRelatedEvents,
+ expectedRelatedEvents);
+ }
+
+ // 1. Empty selection (implicit).
+ assert.lengthOf(tabView.tabs, 0);
+ checkSelectedTab(undefined, []);
+
+ // 2. Event selection: two samples and one thread slice.
+ controller.changeSelectionFromRequestSelectionChangeEvent(
+ new EventSet([sample1, slice1, sample2]));
+ assert.lengthOf(tabView.tabs, 2);
+ const sampleTab2 = tabView.tabs[0];
+ checkTab(sampleTab2,
+ 'tr-ui-a-counter-sample-sub-view',
+ [sample1, sample2]);
+ const singleThreadSliceTab2 = tabView.tabs[1];
+ checkTab(singleThreadSliceTab2,
+ 'tr-ui-a-single-thread-time-slice-sub-view',
+ [slice1]);
+ // First tab should be selected.
+ checkSelectedTab(sampleTab2, []);
+
+ // 3. Tab selection: single thread slice tab.
+ tabView.selectedSubView = singleThreadSliceTab2;
+ checkSelectedTab(singleThreadSliceTab2, []);
+
+ // 4. Event selection: one sample, two thread slices, and one
+ // user expectation.
+ controller.changeSelectionFromRequestSelectionChangeEvent(
+ new EventSet([slice1, slice2, sample3, record1]));
+ assert.lengthOf(tabView.tabs, 3);
+ const sampleTab4 = tabView.tabs[1];
+ checkTab(sampleTab4,
+ 'tr-ui-a-counter-sample-sub-view',
+ [sample3]);
+ const singleRecordTab4 = tabView.tabs[2];
+ checkTab(singleRecordTab4,
+ 'tr-ui-a-single-user-expectation-sub-view',
+ [record1]);
+ const multiThreadSliceTab4 = tabView.tabs[0];
+ checkTab(multiThreadSliceTab4,
+ 'tr-ui-a-multi-thread-time-slice-sub-view',
+ [slice1, slice2]);
+ // Remember selected tab (even though the tab was destroyed).
+ checkSelectedTab(multiThreadSliceTab4, []);
+
+ // 5. Tab selection: single user expectation tab.
+ tabView.selectedSubView = singleRecordTab4;
+ checkSelectedTab(singleRecordTab4, [sample1, slice1]);
+
+ // 6. Event selection: one user expectation.
+ controller.changeSelectionFromRequestSelectionChangeEvent(
+ new EventSet([record2]));
+ assert.lengthOf(tabView.tabs, 1);
+ const singleRecordTab6 = tabView.tabs[0];
+ checkTab(singleRecordTab6,
+ 'tr-ui-a-single-user-expectation-sub-view',
+ [record2]);
+ // Remember selected tab.
+ checkSelectedTab(singleRecordTab6, [sample2, sample3, slice1]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html
new file mode 100644
index 00000000000..cc0e9155358
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_header_pane.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-container-memory-dump-sub-view'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <div id="content"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ Polymer({
+ is: 'tr-ui-a-container-memory-dump-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ set selection(selection) {
+ if (selection === undefined) {
+ this.currentSelection_ = undefined;
+ this.dumpsByContainerName_ = undefined;
+ this.updateContents_();
+ return;
+ }
+
+ // Check that the selection contains only container memory dumps.
+ selection.forEach(function(event) {
+ if (!(event instanceof tr.model.ContainerMemoryDump)) {
+ throw new Error(
+ 'Memory dump sub-view only supports container memory dumps');
+ }
+ });
+ this.currentSelection_ = selection;
+
+ // Group the selected memory dumps by container name and sort them
+ // chronologically.
+ this.dumpsByContainerName_ = tr.b.groupIntoMap(
+ this.currentSelection_.toArray(), dump => dump.containerName);
+ for (const dumps of this.dumpsByContainerName_.values()) {
+ dumps.sort((a, b) => a.start - b.start);
+ }
+
+ this.updateContents_();
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ get requiresTallView() {
+ return true;
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.content).textContent = '';
+
+ if (this.dumpsByContainerName_ === undefined) return;
+
+ const containerNames = Array.from(this.dumpsByContainerName_.keys());
+ if (containerNames.length === 0) return;
+
+ if (containerNames.length > 1) {
+ this.buildViewForMultipleContainerNames_();
+ } else {
+ this.buildViewForSingleContainerName_();
+ }
+ },
+
+ buildViewForSingleContainerName_() {
+ const containerMemoryDumps = tr.b.getFirstElement(
+ this.dumpsByContainerName_.values());
+ const dumpView = this.ownerDocument.createElement(
+ 'tr-ui-a-stacked-pane-view');
+ Polymer.dom(this.$.content).appendChild(dumpView);
+ dumpView.setPaneBuilder(function() {
+ const headerPane = document.createElement(
+ 'tr-ui-a-memory-dump-header-pane');
+ headerPane.containerMemoryDumps = containerMemoryDumps;
+ return headerPane;
+ });
+ },
+
+ buildViewForMultipleContainerNames_() {
+ // TODO(petrcermak): Provide a more sophisticated view for this case.
+ const ownerDocument = this.ownerDocument;
+
+ const rows = [];
+ for (const [containerName, dumps] of this.dumpsByContainerName_) {
+ rows.push({
+ containerName,
+ subRows: dumps,
+ isExpanded: true,
+ });
+ }
+ rows.sort(function(a, b) {
+ return a.containerName.localeCompare(b.containerName);
+ });
+
+ const columns = [
+ {
+ title: 'Dump',
+
+ value(row) {
+ if (row.subRows === undefined) {
+ return this.singleDumpValue_(row);
+ }
+ return this.groupedDumpValue_(row);
+ },
+
+ singleDumpValue_(row) {
+ const linkEl = ownerDocument.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(new tr.model.EventSet([row]));
+ Polymer.dom(linkEl).appendChild(tr.v.ui.createScalarSpan(
+ row.start, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument
+ }));
+ return linkEl;
+ },
+
+ groupedDumpValue_(row) {
+ const linkEl = ownerDocument.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(new tr.model.EventSet(row.subRows));
+ Polymer.dom(linkEl).appendChild(tr.ui.b.createSpan({
+ ownerDocument,
+ textContent: row.subRows.length + ' memory dump' +
+ (row.subRows.length === 1 ? '' : 's') + ' in '
+ }));
+ Polymer.dom(linkEl).appendChild(tr.ui.b.createSpan({
+ ownerDocument,
+ textContent: row.containerName,
+ bold: true
+ }));
+ return linkEl;
+ }
+ }
+ ];
+
+ const table = this.ownerDocument.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.showHeader = false;
+ table.rebuild();
+ Polymer.dom(this.$.content).appendChild(table);
+ }
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-container-memory-dump-sub-view',
+ tr.model.GlobalMemoryDump,
+ {
+ multi: false,
+ title: 'Global Memory Dump',
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-container-memory-dump-sub-view',
+ tr.model.GlobalMemoryDump,
+ {
+ multi: true,
+ title: 'Global Memory Dumps',
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-container-memory-dump-sub-view',
+ tr.model.ProcessMemoryDump,
+ {
+ multi: false,
+ title: 'Process Memory Dump',
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-container-memory-dump-sub-view',
+ tr.model.ProcessMemoryDump,
+ {
+ multi: true,
+ title: 'Process Memory Dumps',
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html
new file mode 100644
index 00000000000..974837f545e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/container_memory_dump_sub_view_test.html
@@ -0,0 +1,351 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import"
+ href="/tracing/ui/analysis/container_memory_dump_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const extractVmRegions = tr.ui.analysis.extractVmRegions;
+ const extractMemoryAllocatorDumps =
+ tr.ui.analysis.extractMemoryAllocatorDumps;
+ const extractHeapDumps = tr.ui.analysis.extractHeapDumps;
+
+ function createViewWithSelection(selection, opt_parentElement) {
+ const viewEl = document.createElement(
+ 'tr-ui-a-container-memory-dump-sub-view');
+ if (opt_parentElement) {
+ Polymer.dom(opt_parentElement).appendChild(viewEl);
+ }
+ if (selection === undefined) {
+ viewEl.selection = undefined;
+ } else {
+ // Rotate the list of selected dumps to check that the sub-view sorts
+ // them properly.
+ const length = selection.length;
+ viewEl.selection = new tr.model.EventSet(
+ selection.slice(length / 2, length).concat(
+ selection.slice(0, length / 2)));
+ }
+ return viewEl;
+ }
+
+ function createAndCheckContainerMemoryDumpView(
+ test, containerMemoryDumps, detailsCheckCallback, opt_parentElement) {
+ const viewEl =
+ createViewWithSelection(containerMemoryDumps, opt_parentElement);
+ if (!opt_parentElement) {
+ test.addHTMLOutput(viewEl);
+ }
+
+ // The view should contain a stacked pane view with memory dump header and
+ // overview panes.
+ const stackedPaneViewEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-stacked-pane-view');
+ const headerPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-header-pane');
+ const overviewPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-overview-pane');
+
+ // Check that the header pane and overview pane are correctly set up.
+ const processMemoryDumps = containerMemoryDumps.map(
+ containerDump => containerDump.processMemoryDumps);
+ assert.deepEqual(
+ Array.from(headerPaneEl.containerMemoryDumps), containerMemoryDumps);
+ assert.deepEqual(overviewPaneEl.processMemoryDumps, processMemoryDumps);
+ assert.strictEqual(
+ overviewPaneEl.aggregationMode, headerPaneEl.aggregationMode);
+
+ // Get the overview pane table to drive the details pane checks.
+ const overviewTableEl = tr.ui.b.findDeepElementMatching(
+ overviewPaneEl, 'tr-ui-b-table');
+
+ function checkVmRegionsPane(pid) {
+ const detailsPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ if (pid === undefined) {
+ assert.isUndefined(detailsPaneEl);
+ } else {
+ assert.deepEqual(Array.from(detailsPaneEl.vmRegions),
+ extractVmRegions(processMemoryDumps, pid));
+ assert.strictEqual(
+ detailsPaneEl.aggregationMode, headerPaneEl.aggregationMode);
+ }
+ }
+
+ function checkAllocatorPane(pid, allocatorName, withHeapDetailsPane) {
+ const allocatorDetailsPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-allocator-details-pane');
+ if (pid === undefined) {
+ assert.isUndefined(allocatorDetailsPaneEl);
+ assert.isUndefined(allocatorName); // Test sanity check.
+ assert.isUndefined(withHeapDetailsPane); // Test sanity check.
+ return;
+ }
+
+ assert.deepEqual(
+ Array.from(allocatorDetailsPaneEl.memoryAllocatorDumps),
+ extractMemoryAllocatorDumps(processMemoryDumps, pid, allocatorName));
+ assert.strictEqual(
+ allocatorDetailsPaneEl.aggregationMode, headerPaneEl.aggregationMode);
+
+ const heapDetailsPaneEl = tr.ui.b.findDeepElementMatching(
+ stackedPaneViewEl, 'tr-ui-a-memory-dump-heap-details-pane');
+ if (!withHeapDetailsPane) {
+ assert.isUndefined(heapDetailsPaneEl);
+ return;
+ }
+
+ assert.deepEqual(Array.from(heapDetailsPaneEl.heapDumps),
+ extractHeapDumps(processMemoryDumps, pid, allocatorName));
+ assert.strictEqual(
+ heapDetailsPaneEl.aggregationMode, headerPaneEl.aggregationMode);
+ }
+
+ detailsCheckCallback(
+ overviewTableEl, checkVmRegionsPane, checkAllocatorPane);
+ }
+
+ test('instantiate_empty', function() {
+ // All these views should be completely empty.
+ const unsetViewEl = document.createElement(
+ 'tr-ui-a-container-memory-dump-sub-view');
+ this.addHTMLOutput(unsetViewEl);
+ assert.strictEqual(unsetViewEl.getBoundingClientRect().width, 0);
+ assert.strictEqual(unsetViewEl.getBoundingClientRect().height, 0);
+
+ const undefinedViewEl = createViewWithSelection(undefined);
+ this.addHTMLOutput(undefinedViewEl);
+ assert.strictEqual(undefinedViewEl.getBoundingClientRect().width, 0);
+ assert.strictEqual(undefinedViewEl.getBoundingClientRect().height, 0);
+
+ const emptyViewEl = createViewWithSelection([]);
+ this.addHTMLOutput(emptyViewEl);
+ assert.strictEqual(emptyViewEl.getBoundingClientRect().width, 0);
+ assert.strictEqual(emptyViewEl.getBoundingClientRect().height, 0);
+ });
+
+ test('instantiate_singleGlobalMemoryDump', function() {
+ createAndCheckContainerMemoryDumpView(this,
+ [tr.ui.analysis.createSingleTestGlobalMemoryDump()],
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Total resident of Process 1.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 1;
+ checkVmRegionsPane(1 /* PID */);
+ checkAllocatorPane(undefined);
+
+ // PSS of process 4.
+ overviewTableEl.selectedColumnIndex = 3;
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[2];
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Malloc of process 2.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[1];
+ overviewTableEl.selectedColumnIndex = 10;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'malloc',
+ false /* no heap details pane */);
+ });
+ });
+
+ test('instantiate_multipleGlobalMemoryDumps', function() {
+ createAndCheckContainerMemoryDumpView(this,
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps(),
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Blink of Process 1.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 8;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Peak total resident of Process 4.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[3];
+ overviewTableEl.selectedColumnIndex = 2;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // V8 of Process 3.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[2];
+ overviewTableEl.selectedColumnIndex = 12;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(3 /* PID */, 'v8', true /* heap details pane */);
+ });
+ });
+
+ test('instantiate_singleProcessMemoryDump', function() {
+ createAndCheckContainerMemoryDumpView(this,
+ [tr.ui.analysis.createSingleTestProcessMemoryDump()],
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Tracing of Process 2.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 13;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'tracing',
+ false /* no heap details pane */);
+
+ // Blink of Process 2.
+ overviewTableEl.selectedColumnIndex = 8;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'blink',
+ false /* no heap details pane */);
+
+ // Total resident of Process 2.
+ overviewTableEl.selectedColumnIndex = 1;
+ checkVmRegionsPane(2 /* PID */);
+ checkAllocatorPane(undefined);
+ });
+ });
+
+ test('instantiate_multipleProcessMemoryDumps', function() {
+ createAndCheckContainerMemoryDumpView(this,
+ tr.ui.analysis.createMultipleTestProcessMemoryDumps(),
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Tracing of Process 2.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 13;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'tracing',
+ false /* no heap details pane */);
+
+ // V8 of Process 2.
+ overviewTableEl.selectedColumnIndex = 12;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'v8',
+ false /* no heap details pane */);
+
+ // PSS of Process 2.
+ overviewTableEl.selectedColumnIndex = 3;
+ checkVmRegionsPane(2 /* PID */);
+ checkAllocatorPane(undefined);
+ });
+ });
+
+ test('memory', function() {
+ const containerEl = document.createElement('div');
+ containerEl.brushingStateController =
+ new tr.c.BrushingStateController(undefined);
+
+ // Create the first container memory view.
+ createAndCheckContainerMemoryDumpView(this,
+ [tr.ui.analysis.createSingleTestProcessMemoryDump()],
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // Nothing should be selected initially.
+ assert.isUndefined(overviewTableEl.selectedTableRow);
+ assert.isUndefined(overviewTableEl.selectedColumnIndex);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(undefined);
+
+ // Select V8 of Process 2.
+ overviewTableEl.selectedTableRow = overviewTableEl.tableRows[0];
+ overviewTableEl.selectedColumnIndex = 12;
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'v8',
+ false /* no heap details pane */);
+ }, containerEl);
+
+ // Destroy the first container memory view.
+ Polymer.dom(containerEl).textContent = '';
+
+ // Create the second container memory view.
+ createAndCheckContainerMemoryDumpView(this,
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps(),
+ function(overviewTableEl, checkVmRegionsPane, checkAllocatorPane) {
+ // V8 of Process 2 should still be selected (even though the selection
+ // changed).
+ assert.strictEqual(
+ overviewTableEl.selectedTableRow, overviewTableEl.tableRows[1]);
+ assert.strictEqual(overviewTableEl.selectedColumnIndex, 12);
+ checkVmRegionsPane(undefined);
+ checkAllocatorPane(2 /* PID */, 'v8',
+ false /* no heap details pane */);
+ }, containerEl);
+ });
+
+ test('instantiate_differentProcessMemoryDumps', function() {
+ const globalMemoryDumps =
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps();
+ // 2 dumps in Process 1, 3 dumps in Process 2, and 1 dump in Process 4
+ // (intentionally shuffled to check sorting).
+ const differentProcessDumps = [
+ globalMemoryDumps[1].processMemoryDumps[2],
+ globalMemoryDumps[0].processMemoryDumps[1],
+ globalMemoryDumps[0].processMemoryDumps[2],
+ globalMemoryDumps[1].processMemoryDumps[4],
+ globalMemoryDumps[1].processMemoryDumps[1],
+ globalMemoryDumps[2].processMemoryDumps[2]
+ ];
+
+ const viewEl = createViewWithSelection(differentProcessDumps);
+ this.addHTMLOutput(viewEl);
+
+ const tableEl = tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-table');
+ assert.lengthOf(tableEl.tableRows, 3);
+ assert.lengthOf(tableEl.tableColumns, 1);
+ const rows = tableEl.tableRows;
+ const col = tableEl.tableColumns[0];
+
+ assert.strictEqual(Polymer.dom(col.value(rows[0])).textContent,
+ '2 memory dumps in Process 1');
+ assert.strictEqual(Polymer.dom(col.value(rows[1])).textContent,
+ '3 memory dumps in Process 2');
+ assert.strictEqual(Polymer.dom(col.value(rows[2])).textContent,
+ '1 memory dump in Process 4');
+
+ // Check that the analysis link is associated with the right dumps.
+ assert.isTrue(col.value(rows[1]).selection.equals(new tr.model.EventSet([
+ globalMemoryDumps[0].processMemoryDumps[2],
+ globalMemoryDumps[1].processMemoryDumps[2],
+ globalMemoryDumps[2].processMemoryDumps[2]
+ ])));
+
+ assert.lengthOf(rows[1].subRows, 3);
+ const subRow = rows[1].subRows[0];
+
+ // Check the timestamp.
+ assert.strictEqual(col.value(subRow).children[0].value, 42);
+
+ // Check that the analysis link is associated with the right dump.
+ assert.isTrue(col.value(subRow).selection.equals(
+ new tr.model.EventSet(globalMemoryDumps[0].processMemoryDumps[2])));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html
new file mode 100644
index 00000000000..a9275b19d0c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-counter-sample-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id='table'></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+(function() {
+ const COUNTER_SAMPLE_TABLE_COLUMNS = [
+ {
+ title: 'Counter',
+ width: '150px',
+ value(row) { return row.counter; }
+ },
+ {
+ title: 'Series',
+ width: '150px',
+ value(row) { return row.series; }
+ },
+ {
+ title: 'Time',
+ width: '150px',
+ value(row) { return row.start; }
+ },
+ {
+ title: 'Value',
+ width: '100%',
+ value(row) { return row.value; }
+ }
+ ];
+
+ Polymer({
+ is: 'tr-ui-a-counter-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ this.$.table.tableColumns = COUNTER_SAMPLE_TABLE_COLUMNS;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.$.table.tableRows =
+ this.selection ? this.getRows_(this.selection.toArray()) : [];
+ this.$.table.rebuild();
+ },
+
+ /**
+ * Returns the table rows for the specified samples.
+ *
+ * We print each counter/series combination the first time that it
+ * appears. For subsequent samples in each series, we omit the counter
+ * and series name. This makes it easy to scan to find the next series.
+ *
+ * Each series can be collapsed. In the expanded state, all samples
+ * are shown. In the collapsed state, only the first sample is displayed.
+ */
+ getRows_(samples) {
+ const samplesByCounter = tr.b.groupIntoMap(
+ samples, sample => sample.series.counter.guid);
+
+ const rows = [];
+ for (const counterSamples of samplesByCounter.values()) {
+ const samplesBySeries = tr.b.groupIntoMap(
+ counterSamples, sample => sample.series.guid);
+
+ for (const seriesSamples of samplesBySeries.values()) {
+ const seriesRows = this.getRowsForSamples_(seriesSamples);
+ seriesRows[0].counter = seriesSamples[0].series.counter.name;
+ seriesRows[0].series = seriesSamples[0].series.name;
+
+ if (seriesRows.length > 1) {
+ seriesRows[0].subRows = seriesRows.slice(1);
+ seriesRows[0].isExpanded = true;
+ }
+
+ rows.push(seriesRows[0]);
+ }
+ }
+
+ return rows;
+ },
+
+ getRowsForSamples_(samples) {
+ return samples.map(function(sample) {
+ return {
+ start: sample.timestamp,
+ value: sample.value
+ };
+ });
+ }
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-counter-sample-sub-view',
+ tr.model.CounterSample,
+ {
+ multi: false,
+ title: 'Counter Sample',
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-counter-sample-sub-view',
+ tr.model.CounterSample,
+ {
+ multi: true,
+ title: 'Counter Samples',
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html
new file mode 100644
index 00000000000..9d7fa370313
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/counter_sample_sub_view_test.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/counter.html">
+<link rel="import" href="/tracing/model/counter_series.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/counter_sample_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Counter = tr.model.Counter;
+ const CounterSeries = tr.model.CounterSeries;
+ const EventSet = tr.model.EventSet;
+
+ test('instantiate_undefinedSelection', function() {
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection = new EventSet(undefined);
+
+ assert.lengthOf(analysisEl.$.table.tableRows, 0);
+ });
+
+ test('instantiate_oneCounterOneSeries', function() {
+ const series = new CounterSeries('series1', 0);
+ series.addCounterSample(0, 0);
+ series.addCounterSample(1, 10);
+
+ const counter = new Counter(null, 0, 'cat', 'ctr1');
+ counter.addSeries(series);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection = new EventSet(series.samples);
+ this.addHTMLOutput(analysisEl);
+
+ // The first sample should be listed as a collapsible header row for the
+ // series.
+ const rows = analysisEl.$.table.tableRows;
+ assert.lengthOf(rows, 1);
+ assert.isTrue(rows[0].isExpanded);
+ assert.strictEqual(rows[0].counter, 'ctr1');
+ assert.strictEqual(rows[0].series, 'series1');
+ assert.strictEqual(rows[0].start, 0);
+ assert.strictEqual(rows[0].value, 0);
+
+ // The second sample should be listed as a subrow of the first.
+ const subRows = rows[0].subRows;
+ assert.lengthOf(subRows, 1);
+ assert.isUndefined(subRows[0].counter);
+ assert.isUndefined(subRows[0].series);
+ assert.strictEqual(subRows[0].start, 1);
+ assert.strictEqual(subRows[0].value, 10);
+ });
+
+ test('instantiate_singleSampleDoesntHaveSubrows', function() {
+ const series = new CounterSeries('series1', 0);
+ series.addCounterSample(0, 0);
+
+ const counter = new Counter(null, 0, 'cat', 'ctr1');
+ counter.addSeries(series);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection = new EventSet(series.samples);
+ this.addHTMLOutput(analysisEl);
+
+ // The first sample should be listed as a collapsible header row for the
+ // series.
+ const rows = analysisEl.$.table.tableRows;
+ assert.lengthOf(rows, 1);
+ assert.strictEqual(rows[0].counter, 'ctr1');
+ assert.strictEqual(rows[0].series, 'series1');
+ assert.strictEqual(rows[0].start, 0);
+ assert.strictEqual(rows[0].value, 0);
+ assert.isUndefined(rows[0].subRows);
+ });
+
+ test('instantiate_oneCounterTwoSeries', function() {
+ const series1 = new CounterSeries('series1', 0);
+ series1.addCounterSample(1, 10);
+ series1.addCounterSample(2, 20);
+
+ const series2 = new CounterSeries('series2', 0);
+ series2.addCounterSample(3, 30);
+
+ const counter = new Counter(null, 0, 'cat', 'ctr1');
+ counter.addSeries(series1);
+ counter.addSeries(series2);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection =
+ new EventSet(series1.samples.concat(series2.samples));
+ this.addHTMLOutput(analysisEl);
+
+ // The first samples should be listed as collapsible header rows for the
+ // series.
+ const rows = analysisEl.$.table.tableRows;
+ assert.lengthOf(rows, 2);
+ assert.strictEqual(rows[0].counter, 'ctr1');
+ assert.strictEqual(rows[0].series, 'series1');
+ assert.strictEqual(rows[0].start, 1);
+ assert.strictEqual(rows[0].value, 10);
+
+ assert.strictEqual(rows[1].counter, 'ctr1');
+ assert.strictEqual(rows[1].series, 'series2');
+ assert.strictEqual(rows[1].start, 3);
+ assert.strictEqual(rows[1].value, 30);
+
+ // The subsequent samples should be listed as subrows of the first.
+ const subRows1 = rows[0].subRows;
+ assert.lengthOf(subRows1, 1);
+ assert.isUndefined(subRows1[0].counter);
+ assert.isUndefined(subRows1[0].series);
+ assert.strictEqual(subRows1[0].start, 2);
+ assert.strictEqual(subRows1[0].value, 20);
+
+ assert.isUndefined(rows[1].subRows);
+ });
+
+ test('instantiate_twoCountersTwoSeries', function() {
+ const series1 = new CounterSeries('series1', 0);
+ series1.addCounterSample(1, 10);
+
+ const series2 = new CounterSeries('series2', 0);
+ series2.addCounterSample(2, 20);
+
+ const counter1 = new Counter(null, 0, 'cat', 'ctr1');
+ const counter2 = new Counter(null, 0, 'cat', 'ctr2');
+ counter1.addSeries(series1);
+ counter2.addSeries(series2);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection =
+ new EventSet(series1.samples.concat(series2.samples));
+ this.addHTMLOutput(analysisEl);
+
+ // Each sample should be a header row with no subrows.
+ const rows = analysisEl.$.table.tableRows;
+ assert.lengthOf(rows, 2);
+ assert.strictEqual(rows[0].counter, 'ctr1');
+ assert.strictEqual(rows[0].series, 'series1');
+ assert.strictEqual(rows[0].start, 1);
+ assert.strictEqual(rows[0].value, 10);
+ assert.isUndefined(rows[0].subRows);
+
+ assert.strictEqual(rows[1].counter, 'ctr2');
+ assert.strictEqual(rows[1].series, 'series2');
+ assert.strictEqual(rows[1].start, 2);
+ assert.strictEqual(rows[1].value, 20);
+ assert.isUndefined(rows[1].subRows);
+ });
+
+ test('instantiate_contentsClearedEachSelection', function() {
+ const series = new CounterSeries('series1', 0);
+ series.addCounterSample(0, 0);
+
+ const counter = new Counter(null, 0, 'cat', 'ctr1');
+ counter.addSeries(series);
+
+ const analysisEl = document.createElement(
+ 'tr-ui-a-counter-sample-sub-view');
+ analysisEl.selection = new EventSet(series.samples);
+ analysisEl.selection = new EventSet(series.samples);
+ this.addHTMLOutput(analysisEl);
+
+ assert.lengthOf(analysisEl.$.table.tableRows, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html
new file mode 100644
index 00000000000..1773a09f32f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const FLOW_IN = 0x1;
+ const FLOW_OUT = 0x2;
+ const FLOW_IN_OUT = FLOW_IN | FLOW_OUT;
+
+ function FlowClassifier() {
+ this.numEvents_ = 0;
+ this.eventsByGUID_ = {};
+ }
+
+ FlowClassifier.prototype = {
+ getFS_(event) {
+ let fs = this.eventsByGUID_[event.guid];
+ if (fs === undefined) {
+ this.numEvents_++;
+ fs = {
+ state: 0,
+ event
+ };
+ this.eventsByGUID_[event.guid] = fs;
+ }
+ return fs;
+ },
+
+ addInFlow(event) {
+ const fs = this.getFS_(event);
+ fs.state |= FLOW_IN;
+ return event;
+ },
+
+ addOutFlow(event) {
+ const fs = this.getFS_(event);
+ fs.state |= FLOW_OUT;
+ return event;
+ },
+
+ hasEvents() {
+ return this.numEvents_ > 0;
+ },
+
+ get inFlowEvents() {
+ const selection = new tr.model.EventSet();
+ for (const guid in this.eventsByGUID_) {
+ const fs = this.eventsByGUID_[guid];
+ if (fs.state === FLOW_IN) {
+ selection.push(fs.event);
+ }
+ }
+ return selection;
+ },
+
+ get outFlowEvents() {
+ const selection = new tr.model.EventSet();
+ for (const guid in this.eventsByGUID_) {
+ const fs = this.eventsByGUID_[guid];
+ if (fs.state === FLOW_OUT) {
+ selection.push(fs.event);
+ }
+ }
+ return selection;
+ },
+
+ get internalFlowEvents() {
+ const selection = new tr.model.EventSet();
+ for (const guid in this.eventsByGUID_) {
+ const fs = this.eventsByGUID_[guid];
+ if (fs.state === FLOW_IN_OUT) {
+ selection.push(fs.event);
+ }
+ }
+ return selection;
+ }
+ };
+
+ return {
+ FlowClassifier,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html
new file mode 100644
index 00000000000..ba68f671b57
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/flow_classifier_test.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/flow_classifier.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+
+ test('basic', function() {
+ const a = newFlowEventEx({
+ title: 'a', start: 0, end: 10 });
+ const b = newFlowEventEx({
+ title: 'b', start: 10, end: 20 });
+ const c = newFlowEventEx({
+ title: 'c', start: 20, end: 25 });
+ const d = newFlowEventEx({
+ title: 'd', start: 30, end: 35 });
+
+ const fc = new tr.ui.analysis.FlowClassifier();
+ fc.addInFlow(a);
+
+ fc.addInFlow(b);
+ fc.addOutFlow(b);
+
+ fc.addInFlow(c);
+ fc.addOutFlow(c);
+
+ fc.addOutFlow(d);
+
+ function asSortedArray(selection) {
+ const events = Array.from(selection);
+ events.sort(function(a, b) {
+ return a.guid - b.guid;
+ });
+ return events;
+ }
+
+ assert.deepEqual(Array.from(fc.inFlowEvents), [a]);
+ assert.deepEqual(Array.from(fc.outFlowEvents), [d]);
+ assert.deepEqual(Array.from(fc.internalFlowEvents), [b, c]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html
new file mode 100644
index 00000000000..bc3f4fcead8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+
+<!--
+@fileoverview A line chart showing milliseconds since the start of the frame on
+the x-axis and power consumption on the y-axis. Each frame is shown as a
+separate line in the chart. Vertical sync events are used as the start of each
+frame.
+
+This chart aims to help users understand the shape of the power consumption
+curve over the course of a frame or set of frames.
+-->
+<dom-module id='tr-ui-a-frame-power-usage-chart'>
+ <template>
+ <div id="content"></div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+const EventSet = tr.model.EventSet;
+
+const CHART_TITLE = 'Power (W) by ms since vertical sync';
+
+Polymer({
+ is: 'tr-ui-a-frame-power-usage-chart',
+
+ ready() {
+ this.chart_ = undefined;
+ this.samples_ = new EventSet();
+ this.vSyncTimestamps_ = [];
+ },
+
+ attached() {
+ if (this.samples_) this.updateContents_();
+ },
+
+ get chart() {
+ return this.chart_;
+ },
+
+ get samples() {
+ return this.samples_;
+ },
+
+ get vSyncTimestamps() {
+ return this.vSyncTimestamps_;
+ },
+
+ /**
+ * Sets the data that powers the chart. Vsync timestamps must be in
+ * chronological order.
+ */
+ setData(samples, vSyncTimestamps) {
+ this.samples_ = (samples === undefined) ? new EventSet() : samples;
+ this.vSyncTimestamps_ =
+ (vSyncTimestamps === undefined) ? [] : vSyncTimestamps;
+ if (this.isAttached) this.updateContents_();
+ },
+
+ updateContents_() {
+ this.clearChart_();
+
+ const data = this.getDataForLineChart_();
+
+ if (data.length === 0) return;
+
+ this.chart_ = new tr.ui.b.LineChart();
+ Polymer.dom(this.$.content).appendChild(this.chart_);
+ this.chart_.chartTitle = CHART_TITLE;
+ this.chart_.data = data;
+ },
+
+ clearChart_() {
+ const content = this.$.content;
+ while (Polymer.dom(content).firstChild) {
+ Polymer.dom(content).removeChild(Polymer.dom(content).firstChild);
+ }
+
+ this.chart_ = undefined;
+ },
+
+ // TODO(charliea): Limit the ms since vsync to the median frame length. The
+ // vertical syncs are not 100% regular and highlighting any sample that's
+ // in one of these 'vertical sync lulls' makes the x-axis have a much larger
+ // scale than it should, effectively squishing the other samples into the
+ // left side of the chart.
+ /**
+ * Returns an array of data points for the chart. Each element in the array
+ * is of the form { x: <ms since vsync>, f<frame#>: <power in mW> }.
+ */
+ getDataForLineChart_() {
+ const sortedSamples = this.sortSamplesByTimestampAscending_(this.samples);
+ const vSyncTimestamps = this.vSyncTimestamps.slice();
+
+ let lastVSyncTimestamp = undefined;
+ const points = [];
+
+ // For each power sample, find and record the frame number that it belongs
+ // to as well as the amount of time elapsed since that frame began.
+ let frameNumber = 0;
+ sortedSamples.forEach(function(sample) {
+ while (vSyncTimestamps.length > 0 && vSyncTimestamps[0] <= sample.start) {
+ lastVSyncTimestamp = vSyncTimestamps.shift();
+ frameNumber++;
+ }
+
+ // If no vertical sync occurred before the power sample, don't use the
+ // power sample.
+ if (lastVSyncTimestamp === undefined) return;
+
+ const point = { x: sample.start - lastVSyncTimestamp };
+ point['f' + frameNumber] = sample.powerInW;
+ points.push(point);
+ });
+
+ return points;
+ },
+
+ sortSamplesByTimestampAscending_(samples) {
+ return samples.toArray().sort(function(smpl1, smpl2) {
+ return smpl1.start - smpl2.start;
+ });
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html
new file mode 100644
index 00000000000..caf4601f33c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_perf_test.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_sample.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function instantiateManyFrames() {
+ const model = new tr.Model();
+ const numFrames = 200;
+ const samplesPerFrame = 200;
+
+ // Set up the test data.
+ const series = new tr.model.PowerSeries(model.device);
+ const vsyncTimestamps = [];
+ for (let i = 0; i < numFrames; i++) {
+ vsyncTimestamps.push(i * samplesPerFrame);
+ for (let j = 0; j < samplesPerFrame; j++) {
+ series.addPowerSample(vsyncTimestamps[i] + j, j);
+ }
+ }
+ const samples = series.samples;
+
+ // Display the chart.
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(samples), vsyncTimestamps);
+ this.addHTMLOutput(chart);
+ }
+
+ timedPerfTest('frame_power_usage_chart', instantiateManyFrames, {
+ iterations: 1
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html
new file mode 100644
index 00000000000..04ba9388852
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/frame_power_usage_chart_test.html
@@ -0,0 +1,267 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_sample.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_noSamples', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(undefined, [0]);
+
+ assert.isUndefined(chart.chart);
+ });
+
+ test('instantiate_noVSyncs', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), []);
+
+ assert.isUndefined(chart.chart);
+ });
+
+ test('instantiate_noSamplesOrVSyncs', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(undefined, []);
+
+ assert.isUndefined(chart.chart);
+ });
+
+ test('instantiate_oneFrame', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [0];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_twoFrames', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [0, 4];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 },
+ { x: 0, f2: 2 },
+ { x: 1, f2: 3 },
+ { x: 2, f2: 4 },
+ { x: 3, f2: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_twoFramesDifferentXValues', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ // Power samples taken at 0, 1, 2, and 3s after frame start.
+ const vSyncTimestamps = [0, 4];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ // Power samples taken at 0.5, 1.5, 2.5, and 3.5s after frame start.
+ series.addPowerSample(4.5, 2);
+ series.addPowerSample(5.5, 3);
+ series.addPowerSample(6.5, 4);
+ series.addPowerSample(7.5, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 },
+ { x: 0.5, f2: 2 },
+ { x: 1.5, f2: 3 },
+ { x: 2.5, f2: 4 },
+ { x: 3.5, f2: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_samplesBeforeFirstVSync', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [4];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 2 },
+ { x: 1, f1: 3 },
+ { x: 2, f1: 4 },
+ { x: 3, f1: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_allSamplesBeforeFirstVSync', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [4];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ const expectedChartData = [
+ { x: 0, f1: 2 },
+ { x: 1, f1: 3 },
+ { x: 2, f1: 4 },
+ { x: 3, f1: 3 }
+ ];
+ assert.isUndefined(chart.chart);
+ });
+
+ test('instantiate_vSyncsAfterLastPowerSample', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [0, 4, 8, 12];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 },
+ { x: 0, f2: 2 },
+ { x: 1, f2: 3 },
+ { x: 2, f2: 4 },
+ { x: 3, f2: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+
+ test('instantiate_onlyVSyncAfterLastPowerSample', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [8];
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ assert.isUndefined(chart.chart);
+ });
+
+
+ test('instantiate_samplesNotInChronologicalOrder', function() {
+ const series = new tr.model.PowerSeries(new tr.Model().device);
+
+ const vSyncTimestamps = [0, 4];
+ series.addPowerSample(4, 2);
+ series.addPowerSample(5, 3);
+ series.addPowerSample(6, 4);
+ series.addPowerSample(7, 3);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1, 2);
+ series.addPowerSample(2, 3);
+ series.addPowerSample(3, 2);
+
+ const chart = document.createElement('tr-ui-a-frame-power-usage-chart');
+ chart.setData(new tr.model.EventSet(series.samples), vSyncTimestamps);
+
+ this.addHTMLOutput(chart);
+
+ const expectedChartData = [
+ { x: 0, f1: 1 },
+ { x: 1, f1: 2 },
+ { x: 2, f1: 3 },
+ { x: 3, f1: 2 },
+ { x: 0, f2: 2 },
+ { x: 1, f2: 3 },
+ { x: 2, f2: 4 },
+ { x: 3, f2: 3 }
+ ];
+ assert.sameDeepMembers(chart.chart.data, expectedChartData);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html
new file mode 100644
index 00000000000..e0c33df1231
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view.html
@@ -0,0 +1,347 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+<link rel="import" href="/tracing/model/object_snapshot.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-generic-object-view'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ font-family: monospace;
+ }
+ </style>
+ <div id="content">
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+function isTable(object) {
+ if (!(object instanceof Array) ||
+ (object.length < 2)) return false;
+ for (const colName in object[0]) {
+ if (typeof colName !== 'string') return false;
+ }
+ for (let i = 0; i < object.length; ++i) {
+ if (!(object[i] instanceof Object)) return false;
+ for (const colName in object[i]) {
+ if (i && (object[0][colName] === undefined)) return false;
+ const cellType = typeof object[i][colName];
+ if (cellType !== 'string' && cellType !== 'number') return false;
+ }
+ if (i) {
+ for (const colName in object[0]) {
+ if (object[i][colName] === undefined) return false;
+ }
+ }
+ }
+ return true;
+}
+
+Polymer({
+ is: 'tr-ui-a-generic-object-view',
+
+ ready() {
+ this.object_ = undefined;
+ },
+
+ get object() {
+ return this.object_;
+ },
+
+ set object(object) {
+ this.object_ = object;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.content).textContent = '';
+ this.appendElementsForType_('', this.object_, 0, 0, 5, '');
+ },
+
+ appendElementsForType_(
+ label, object, indent, depth, maxDepth, suffix) {
+ if (depth > maxDepth) {
+ this.appendSimpleText_(
+ label, indent, '<recursion limit reached>', suffix);
+ return;
+ }
+
+ if (object === undefined) {
+ this.appendSimpleText_(label, indent, 'undefined', suffix);
+ return;
+ }
+
+ if (object === null) {
+ this.appendSimpleText_(label, indent, 'null', suffix);
+ return;
+ }
+
+ if (!(object instanceof Object)) {
+ const type = typeof object;
+ if (type !== 'string') {
+ return this.appendSimpleText_(label, indent, object, suffix);
+ }
+ let objectReplaced = false;
+ if ((object[0] === '{' && object[object.length - 1] === '}') ||
+ (object[0] === '[' && object[object.length - 1] === ']')) {
+ try {
+ object = JSON.parse(object);
+ objectReplaced = true;
+ } catch (e) {
+ }
+ }
+ if (!objectReplaced) {
+ if (object.includes('\n')) {
+ const lines = object.split('\n');
+ lines.forEach(function(line, i) {
+ let text;
+ let ioff;
+ let ll;
+ let ss;
+ if (i === 0) {
+ text = '"' + line;
+ ioff = 0;
+ ll = label;
+ ss = '';
+ } else if (i < lines.length - 1) {
+ text = line;
+ ioff = 1;
+ ll = '';
+ ss = '';
+ } else {
+ text = line + '"';
+ ioff = 1;
+ ll = '';
+ ss = suffix;
+ }
+
+ const el = this.appendSimpleText_(
+ ll, indent + ioff * label.length + ioff, text, ss);
+ el.style.whiteSpace = 'pre';
+ return el;
+ }, this);
+ return;
+ }
+ if (tr.b.isUrl(object)) {
+ const link = document.createElement('a');
+ link.href = object;
+ link.textContent = object;
+ this.appendElementWithLabel_(label, indent, link, suffix);
+ return;
+ }
+ this.appendSimpleText_(
+ label, indent, '"' + object + '"', suffix);
+ return;
+ }
+ }
+
+ if (object instanceof tr.model.ObjectSnapshot) {
+ const link = document.createElement('tr-ui-a-analysis-link');
+ link.selection = new tr.model.EventSet(object);
+ this.appendElementWithLabel_(label, indent, link, suffix);
+ return;
+ }
+
+ if (object instanceof tr.model.ObjectInstance) {
+ const link = document.createElement('tr-ui-a-analysis-link');
+ link.selection = new tr.model.EventSet(object);
+ this.appendElementWithLabel_(label, indent, link, suffix);
+ return;
+ }
+
+ if (object instanceof tr.b.math.Rect) {
+ this.appendSimpleText_(label, indent, object.toString(), suffix);
+ return;
+ }
+
+ if (object instanceof tr.b.Scalar) {
+ const el = this.ownerDocument.createElement('tr-v-ui-scalar-span');
+ el.value = object;
+ el.inline = true;
+ this.appendElementWithLabel_(label, indent, el, suffix);
+ return;
+ }
+
+ if (object instanceof Array) {
+ this.appendElementsForArray_(
+ label, object, indent, depth, maxDepth, suffix);
+ return;
+ }
+
+ this.appendElementsForObject_(
+ label, object, indent, depth, maxDepth, suffix);
+ },
+
+ appendElementsForArray_(
+ label, object, indent, depth, maxDepth, suffix) {
+ if (object.length === 0) {
+ this.appendSimpleText_(label, indent, '[]', suffix);
+ return;
+ }
+
+ if (isTable(object)) {
+ const table = document.createElement('tr-ui-b-table');
+ const columns = [];
+ for (const colName of Object.keys(object[0])) {
+ let allStrings = true;
+ let allNumbers = true;
+ for (let i = 0; i < object.length; ++i) {
+ if (typeof(object[i][colName]) !== 'string') {
+ allStrings = false;
+ }
+
+ if (typeof(object[i][colName]) !== 'number') {
+ allNumbers = false;
+ }
+
+ if (!allStrings && !allNumbers) break;
+ }
+
+ const column = {title: colName};
+ column.value = function(row) {
+ return row[colName];
+ };
+
+ if (allStrings) {
+ column.cmp = function(x, y) {
+ return x[colName].localeCompare(y[colName]);
+ };
+ } else if (allNumbers) {
+ column.cmp = function(x, y) {
+ return x[colName] - y[colName];
+ };
+ }
+ columns.push(column);
+ }
+ table.tableColumns = columns;
+ table.tableRows = object;
+ this.appendElementWithLabel_(label, indent, table, suffix);
+ table.rebuild();
+ return;
+ }
+
+ this.appendElementsForType_(
+ label + '[',
+ object[0],
+ indent, depth + 1, maxDepth,
+ object.length > 1 ? ',' : ']' + suffix);
+ for (let i = 1; i < object.length; i++) {
+ this.appendElementsForType_(
+ '',
+ object[i],
+ indent + label.length + 1, depth + 1, maxDepth,
+ i < object.length - 1 ? ',' : ']' + suffix);
+ }
+ return;
+ },
+
+ appendElementsForObject_(
+ label, object, indent, depth, maxDepth, suffix) {
+ const keys = Object.keys(object);
+ if (keys.length === 0) {
+ this.appendSimpleText_(label, indent, '{}', suffix);
+ return;
+ }
+
+ this.appendElementsForType_(
+ label + '{' + keys[0] + ': ',
+ object[keys[0]],
+ indent, depth, maxDepth,
+ keys.length > 1 ? ',' : '}' + suffix);
+ for (let i = 1; i < keys.length; i++) {
+ this.appendElementsForType_(
+ keys[i] + ': ',
+ object[keys[i]],
+ indent + label.length + 1, depth + 1, maxDepth,
+ i < keys.length - 1 ? ',' : '}' + suffix);
+ }
+ },
+
+ appendElementWithLabel_(label, indent, dataElement, suffix) {
+ const row = document.createElement('div');
+
+ const indentSpan = document.createElement('span');
+ indentSpan.style.whiteSpace = 'pre';
+ for (let i = 0; i < indent; i++) {
+ Polymer.dom(indentSpan).textContent += ' ';
+ }
+ Polymer.dom(row).appendChild(indentSpan);
+
+ const labelSpan = document.createElement('span');
+ Polymer.dom(labelSpan).textContent = label;
+ Polymer.dom(row).appendChild(labelSpan);
+
+ Polymer.dom(row).appendChild(dataElement);
+ const suffixSpan = document.createElement('span');
+ Polymer.dom(suffixSpan).textContent = suffix;
+ Polymer.dom(row).appendChild(suffixSpan);
+
+ row.dataElement = dataElement;
+ Polymer.dom(this.$.content).appendChild(row);
+ },
+
+ appendSimpleText_(label, indent, text, suffix) {
+ const el = this.ownerDocument.createElement('span');
+ Polymer.dom(el).textContent = text;
+ this.appendElementWithLabel_(label, indent, el, suffix);
+ return el;
+ }
+});
+</script>
+
+<dom-module id='tr-ui-a-generic-object-view-with-label'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ </style>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-generic-object-view-with-label',
+
+ ready() {
+ this.labelEl_ = document.createElement('div');
+ this.genericObjectView_ =
+ document.createElement('tr-ui-a-generic-object-view');
+ Polymer.dom(this.root).appendChild(this.labelEl_);
+ Polymer.dom(this.root).appendChild(this.genericObjectView_);
+ },
+
+ get label() {
+ return Polymer.dom(this.labelEl_).textContent;
+ },
+
+ set label(label) {
+ Polymer.dom(this.labelEl_).textContent = label;
+ },
+
+ get object() {
+ return this.genericObjectView_.object;
+ },
+
+ set object(object) {
+ this.genericObjectView_.object = object;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html
new file mode 100644
index 00000000000..2e7812e2730
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/generic_object_view_test.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/object_instance.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('undefinedValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = undefined;
+ assert.strictEqual(Polymer.dom(view.$.content).textContent, 'undefined');
+ });
+
+ test('nullValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = null;
+ assert.strictEqual(Polymer.dom(view.$.content).textContent, 'null');
+ });
+
+ test('stringValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = 'string value';
+ assert.strictEqual(
+ Polymer.dom(view.$.content).textContent, '"string value"');
+ });
+
+ test('multiLineStringValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = 'i am a\n string value\ni have\n various indents';
+ this.addHTMLOutput(view);
+ const c = view.$.content;
+ });
+
+ test('multiLineStringValueInsideObject', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = {key: 'i am a\n string value\ni have\n various indents',
+ value: 'simple'};
+ this.addHTMLOutput(view);
+ const c = view.$.content;
+ });
+
+ test('jsonObjectStringValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = '{"x": 1}';
+ assert.strictEqual(view.$.content.children.length, 1);
+ assert.strictEqual(view.$.content.children[0].children.length, 4);
+ });
+
+ test('jsonArrayStringValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = '[1,2,3]';
+ assert.strictEqual(view.$.content.children.length, 3);
+ });
+
+ test('booleanValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = false;
+ assert.strictEqual(Polymer.dom(view.$.content).textContent, 'false');
+ });
+
+ test('numberValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = 3.14159;
+ assert.strictEqual(Polymer.dom(view.$.content).textContent, '3.14159');
+ });
+
+ test('objectSnapshotValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ view.object = s10;
+ this.addHTMLOutput(view);
+ assert.strictEqual(view.$.content.children[0].dataElement.tagName,
+ 'TR-UI-A-ANALYSIS-LINK');
+ });
+
+ test('objectInstanceValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+
+ view.object = i10;
+ assert.strictEqual(view.$.content.children[0].dataElement.tagName,
+ 'TR-UI-A-ANALYSIS-LINK');
+ });
+
+ test('instantiate_emptyArrayValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_twoValueArrayValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [1, 2];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_twoValueBArrayValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [1, {x: 1}];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_arrayValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [1, 2, 'three'];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_arrayWithSimpleObjectValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [{simple: 'object'}];
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_arrayWithComplexObjectValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [{col0: 'object', col1: 0},
+ {col2: 'Object', col3: 1}];
+ this.addHTMLOutput(view);
+ assert.strictEqual(undefined, tr.ui.b.findDeepElementMatching(
+ view.$.content, 'table'));
+ });
+
+ test('instantiate_arrayWithDeepObjectValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [{key: {deep: 'object values make isTable() return false'}}];
+ this.addHTMLOutput(view);
+ assert.strictEqual(undefined, tr.ui.b.findDeepElementMatching(
+ view.$.content, 'table'));
+ });
+
+ test('jsonTableValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = [
+ {col0: 'object', col1: 0, col2: 'foo'},
+ {col0: 'Object', col1: 1, col2: 42}
+ ];
+ this.addHTMLOutput(view);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ view.$.content, 'tr-ui-b-table');
+ assert.strictEqual('col0', table.tableColumns[0].title);
+ assert.strictEqual('col1', table.tableColumns[1].title);
+ assert.strictEqual(
+ 'object', table.tableColumns[0].value(table.tableRows[0]));
+ assert.strictEqual(
+ 'Object', table.tableColumns[0].value(table.tableRows[1]));
+ assert.strictEqual(0, table.tableColumns[1].value(table.tableRows[0]));
+ assert.strictEqual(1, table.tableColumns[1].value(table.tableRows[1]));
+ assert.isDefined(table.tableColumns[0].cmp);
+ assert.isDefined(table.tableColumns[1].cmp);
+ assert.isUndefined(table.tableColumns[2].cmp);
+ });
+
+ test('instantiate_objectValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = {
+ 'entry_one': 'entry_one_value',
+ 'entry_two': 2,
+ 'entry_three': [3, 4, 5]
+ };
+ this.addHTMLOutput(view);
+ });
+
+ test('timeDurationValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object =
+ new tr.b.Scalar(tr.b.Unit.byName.timeDurationInMs, 3);
+ this.addHTMLOutput(view);
+ assert.isDefined(tr.ui.b.findDeepElementMatching(
+ view.$.content, 'tr-v-ui-scalar-span'));
+ });
+
+ test('timeStampValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object = new tr.b.Scalar(tr.b.Unit.byName.timeStampInMs, 3);
+ this.addHTMLOutput(view);
+ assert.isDefined(tr.ui.b.findDeepElementMatching(
+ view.$.content, 'tr-v-ui-scalar-span'));
+ });
+
+ test('scalarValue', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ view.object =
+ new tr.b.Scalar(tr.b.Unit.byName.normalizedPercentage, .3);
+ this.addHTMLOutput(view);
+ const m = tr.ui.b.findDeepElementMatching(
+ view.$.content, 'tr-v-ui-scalar-span');
+ assert.isDefined(m);
+ assert.strictEqual(m.value, .3);
+ assert.strictEqual(m.unit, tr.b.Unit.byName.normalizedPercentage);
+ });
+
+ test('httpLink', function() {
+ const view = document.createElement('tr-ui-a-generic-object-view');
+ const url = 'https://google.com/chrome';
+ view.object = {a: url};
+ this.addHTMLOutput(view);
+ const a = tr.ui.b.findDeepElementMatching(view.$.content, 'a');
+ assert.isDefined(a);
+ assert.strictEqual(url, a.href);
+ assert.strictEqual(url, a.textContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html
new file mode 100644
index 00000000000..88b3c3ccc7c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane.html
@@ -0,0 +1,893 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_pane.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+
+<dom-module id='tr-ui-a-memory-dump-allocator-details-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #label {
+ flex: 0 0 auto;
+ padding: 8px;
+
+ background-color: #eee;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ #contents {
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+
+ #info_text {
+ padding: 8px;
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ #table {
+ display: none; /* Hide until memory allocator dumps are set. */
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <div id="label">Component details</div>
+ <div id="contents">
+ <div id="info_text">No memory allocator dump selected</div>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ // Link to docs.
+ const URL_TO_SIZE_VS_EFFECTIVE_SIZE = 'https://chromium.googlesource.com/chromium/src/+/master/docs/memory-infra/README.md#effective_size-vs_size';
+
+ // Constant representing the context in suballocation rows.
+ const SUBALLOCATION_CONTEXT = true;
+
+ // Size numeric info types.
+ const MemoryAllocatorDumpInfoType = tr.model.MemoryAllocatorDumpInfoType;
+ const PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN =
+ MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN;
+ const PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER =
+ MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER;
+
+ // Unicode symbols used for memory cell info icons and messages.
+ const LEFTWARDS_OPEN_HEADED_ARROW = String.fromCharCode(0x21FD);
+ const RIGHTWARDS_OPEN_HEADED_ARROW = String.fromCharCode(0x21FE);
+ const EN_DASH = String.fromCharCode(0x2013);
+ const CIRCLED_LATIN_SMALL_LETTER_I = String.fromCharCode(0x24D8);
+
+ /** @constructor */
+ function AllocatorDumpNameColumn() {
+ tr.ui.analysis.TitleColumn.call(this, 'Component');
+ }
+
+ AllocatorDumpNameColumn.prototype = {
+ __proto__: tr.ui.analysis.TitleColumn.prototype,
+
+ formatTitle(row) {
+ if (!row.suballocation) {
+ return row.title;
+ }
+ return tr.ui.b.createSpan({
+ textContent: row.title,
+ italic: true,
+ tooltip: row.fullNames === undefined ?
+ undefined : row.fullNames.join(', ')
+ });
+ }
+ };
+
+ /**
+ * Retrieve the entry associated with a given name from a map and increment
+ * its count.
+ *
+ * If there is no entry associated with the name, a new entry is created, the
+ * creation callback is called, the entry's count is incremented (from 0 to
+ * 1) and the newly created entry is returned.
+ */
+ function getAndUpdateEntry(map, name, createdCallback) {
+ let entry = map.get(name);
+ if (entry === undefined) {
+ entry = {count: 0};
+ createdCallback(entry);
+ map.set(name, entry);
+ }
+ entry.count++;
+ return entry;
+ }
+
+ /**
+ * Helper class for building size and effective size column info messages.
+ *
+ * @constructor
+ */
+ function SizeInfoMessageBuilder() {
+ this.parts_ = [];
+ this.indent_ = 0;
+ }
+
+ SizeInfoMessageBuilder.prototype = {
+ append(/* arguments */) {
+ this.parts_.push.apply(
+ this.parts_, Array.prototype.slice.apply(arguments));
+ },
+
+ /**
+ * Append the entries of a map to the message according to the following
+ * rules:
+ *
+ * 1. If the map is empty, append emptyText to the message (if provided).
+ * Examples:
+ *
+ * emptyText=undefined
+ * Hello, World! ====================> Hello, World!
+ *
+ * emptyText='empty'
+ * The bottle is ====================> The bottle is empty
+ *
+ * 2. If the map contains a single entry, append a space and call
+ * itemCallback on the entry (which is in turn expected to append a
+ * message for the entry). Example:
+ *
+ * Please do not ====================> Please do not [item-message]
+ *
+ * 3. If the map contains multiple entries, append them as a list
+ * with itemCallback called on each entry. If hasPluralSuffix is true,
+ * 's' will be appended to the message before the list. Examples:
+ *
+ * hasPluralSuffix=false
+ * I need to buy ====================> I need to buy:
+ * - [item1-message]
+ * - [item2-message]
+ * [...]
+ * - [itemN-message]
+ *
+ * hasPluralSuffix=true
+ * Suspected CL ====================> Suspected CLs:
+ * - [item1-message]
+ * - [item2-message]
+ * [...]
+ * - [itemN-message]
+ */
+ appendMap(
+ map, hasPluralSuffix, emptyText, itemCallback, opt_this) {
+ opt_this = opt_this || this;
+ if (map.size === 0) {
+ if (emptyText) {
+ this.append(emptyText);
+ }
+ } else if (map.size === 1) {
+ this.parts_.push(' ');
+ const key = map.keys().next().value;
+ itemCallback.call(opt_this, key, map.get(key));
+ } else {
+ if (hasPluralSuffix) {
+ this.parts_.push('s');
+ }
+ this.parts_.push(':');
+ this.indent_++;
+ for (const key of map.keys()) {
+ this.parts_.push('\n', ' '.repeat(3 * (this.indent_ - 1)), ' - ');
+ itemCallback.call(opt_this, key, map.get(key));
+ }
+ this.indent_--;
+ }
+ },
+
+ appendImportanceRange(range) {
+ this.append(' (importance: ');
+ if (range.min === range.max) {
+ this.append(range.min);
+ } else {
+ this.append(range.min, EN_DASH, range.max);
+ }
+ this.append(')');
+ },
+
+ appendSizeIfDefined(size) {
+ if (size !== undefined) {
+ this.append(' (', tr.b.Unit.byName.sizeInBytes.format(size), ')');
+ }
+ },
+
+ appendSomeTimestampsQuantifier() {
+ this.append(
+ ' ', tr.ui.analysis.MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER);
+ },
+
+ build() {
+ return this.parts_.join('');
+ }
+ };
+
+ /** @constructor */
+ function EffectiveSizeColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.DetailsNumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ EffectiveSizeColumn.prototype = {
+ __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype,
+
+ get title() {
+ return tr.ui.b.createLink({
+ textContent: this.name,
+ tooltip: 'Memory used by this component',
+ href: URL_TO_SIZE_VS_EFFECTIVE_SIZE
+ });
+ },
+
+ addInfos(numerics, memoryAllocatorDumps, infos) {
+ if (memoryAllocatorDumps === undefined) return;
+
+ // Quantified name of an owner dump (of the given dump) -> {count,
+ // importanceRange}.
+ const ownerNameToEntry = new Map();
+
+ // Quantified name of an owned dump (by the given dump) -> {count,
+ // importanceRange, sharerNameToEntry}, where sharerNameToEntry is a map
+ // from quantified names of other owners of the owned dump to {count,
+ // importanceRange}.
+ const ownedNameToEntry = new Map();
+
+ for (let i = 0; i < numerics.length; i++) {
+ if (numerics[i] === undefined) continue;
+
+ const dump = memoryAllocatorDumps[i];
+ if (dump === SUBALLOCATION_CONTEXT) {
+ return; // No ownership of suballocation internal rows.
+ }
+
+ // Gather owners of this dump.
+ dump.ownedBy.forEach(function(ownerLink) {
+ const ownerDump = ownerLink.source;
+ this.getAndUpdateOwnershipEntry_(
+ ownerNameToEntry, ownerDump, ownerLink);
+ }, this);
+
+ // Gather dumps owned by this dump and other owner dumps sharing them
+ // (with this dump).
+ const ownedLink = dump.owns;
+ if (ownedLink !== undefined) {
+ const ownedDump = ownedLink.target;
+ const ownedEntry = this.getAndUpdateOwnershipEntry_(ownedNameToEntry,
+ ownedDump, ownedLink, true /* opt_withSharerNameToEntry */);
+ const sharerNameToEntry = ownedEntry.sharerNameToEntry;
+ ownedDump.ownedBy.forEach(function(sharerLink) {
+ const sharerDump = sharerLink.source;
+ if (sharerDump === dump) return;
+ this.getAndUpdateOwnershipEntry_(
+ sharerNameToEntry, sharerDump, sharerLink);
+ }, this);
+ }
+ }
+
+ // Emit a single info listing all owners of this dump.
+ if (ownerNameToEntry.size > 0) {
+ const messageBuilder = new SizeInfoMessageBuilder();
+ messageBuilder.append('shared by');
+ messageBuilder.appendMap(
+ ownerNameToEntry,
+ false /* hasPluralSuffix */,
+ undefined /* emptyText */,
+ function(ownerName, ownerEntry) {
+ messageBuilder.append(ownerName);
+ if (ownerEntry.count < numerics.length) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ messageBuilder.appendImportanceRange(ownerEntry.importanceRange);
+ }, this);
+ infos.push({
+ message: messageBuilder.build(),
+ icon: LEFTWARDS_OPEN_HEADED_ARROW,
+ color: 'green'
+ });
+ }
+
+ // Emit a single info listing all dumps owned by this dump together
+ // with list(s) of other owner dumps sharing them with this dump.
+ if (ownedNameToEntry.size > 0) {
+ const messageBuilder = new SizeInfoMessageBuilder();
+ messageBuilder.append('shares');
+ messageBuilder.appendMap(
+ ownedNameToEntry,
+ false /* hasPluralSuffix */,
+ undefined /* emptyText */,
+ function(ownedName, ownedEntry) {
+ messageBuilder.append(ownedName);
+ const ownedCount = ownedEntry.count;
+ if (ownedCount < numerics.length) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ messageBuilder.appendImportanceRange(ownedEntry.importanceRange);
+ messageBuilder.append(' with');
+ messageBuilder.appendMap(
+ ownedEntry.sharerNameToEntry,
+ false /* hasPluralSuffix */,
+ ' no other dumps',
+ function(sharerName, sharerEntry) {
+ messageBuilder.append(sharerName);
+ if (sharerEntry.count < ownedCount) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ messageBuilder.appendImportanceRange(
+ sharerEntry.importanceRange);
+ }, this);
+ }, this);
+ infos.push({
+ message: messageBuilder.build(),
+ icon: RIGHTWARDS_OPEN_HEADED_ARROW,
+ color: 'green'
+ });
+ }
+ },
+
+ getAndUpdateOwnershipEntry_(
+ map, dump, link, opt_withSharerNameToEntry) {
+ const entry = getAndUpdateEntry(map, dump.quantifiedName,
+ function(newEntry) {
+ newEntry.importanceRange = new tr.b.math.Range();
+ if (opt_withSharerNameToEntry) {
+ newEntry.sharerNameToEntry = new Map();
+ }
+ });
+ entry.importanceRange.addValue(link.importance || 0);
+ return entry;
+ }
+ };
+
+ /** @constructor */
+ function SizeColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.DetailsNumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ SizeColumn.prototype = {
+ __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype,
+
+ get title() {
+ return tr.ui.b.createLink({
+ textContent: this.name,
+ tooltip: 'Memory requested by this component',
+ href: URL_TO_SIZE_VS_EFFECTIVE_SIZE
+ });
+ },
+
+ addInfos(numerics, memoryAllocatorDumps, infos) {
+ if (memoryAllocatorDumps === undefined) return;
+ this.addOverlapInfo_(numerics, memoryAllocatorDumps, infos);
+ this.addProvidedSizeWarningInfos_(numerics, memoryAllocatorDumps, infos);
+ },
+
+ addOverlapInfo_(numerics, memoryAllocatorDumps, infos) {
+ // Sibling allocator dump name -> {count, size}. The latter field (size)
+ // is omitted in multi-selection mode.
+ const siblingNameToEntry = new Map();
+ for (let i = 0; i < numerics.length; i++) {
+ if (numerics[i] === undefined) continue;
+ const dump = memoryAllocatorDumps[i];
+ if (dump === SUBALLOCATION_CONTEXT) {
+ return; // No ownership of suballocation internal rows.
+ }
+ const ownedBySiblingSizes = dump.ownedBySiblingSizes;
+ for (const siblingDump of ownedBySiblingSizes.keys()) {
+ const siblingName = siblingDump.name;
+ getAndUpdateEntry(siblingNameToEntry, siblingName,
+ function(newEntry) {
+ if (numerics.length === 1 /* single-selection mode */) {
+ newEntry.size = ownedBySiblingSizes.get(siblingDump);
+ }
+ });
+ }
+ }
+
+ // Emit a single info describing all overlaps with siblings (if
+ // applicable).
+ if (siblingNameToEntry.size > 0) {
+ const messageBuilder = new SizeInfoMessageBuilder();
+ messageBuilder.append('overlaps with its sibling');
+ messageBuilder.appendMap(
+ siblingNameToEntry,
+ true /* hasPluralSuffix */,
+ undefined /* emptyText */,
+ function(siblingName, siblingEntry) {
+ messageBuilder.append('\'', siblingName, '\'');
+ messageBuilder.appendSizeIfDefined(siblingEntry.size);
+ if (siblingEntry.count < numerics.length) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ }, this);
+ infos.push({
+ message: messageBuilder.build(),
+ icon: CIRCLED_LATIN_SMALL_LETTER_I,
+ color: 'blue'
+ });
+ }
+ },
+
+ addProvidedSizeWarningInfos_(numerics, memoryAllocatorDumps,
+ infos) {
+ // Info type (see MemoryAllocatorDumpInfoType) -> {count, providedSize,
+ // dependencySize}. The latter two fields (providedSize and
+ // dependencySize) are omitted in multi-selection mode.
+ const infoTypeToEntry = new Map();
+ for (let i = 0; i < numerics.length; i++) {
+ if (numerics[i] === undefined) continue;
+ const dump = memoryAllocatorDumps[i];
+ if (dump === SUBALLOCATION_CONTEXT) {
+ return; // Suballocation internal rows have no provided size.
+ }
+ dump.infos.forEach(function(dumpInfo) {
+ getAndUpdateEntry(infoTypeToEntry, dumpInfo.type, function(newEntry) {
+ if (numerics.length === 1 /* single-selection mode */) {
+ newEntry.providedSize = dumpInfo.providedSize;
+ newEntry.dependencySize = dumpInfo.dependencySize;
+ }
+ });
+ });
+ }
+
+ // Emit a warning info for every info type.
+ for (const infoType of infoTypeToEntry.keys()) {
+ const entry = infoTypeToEntry.get(infoType);
+ const messageBuilder = new SizeInfoMessageBuilder();
+ messageBuilder.append('provided size');
+ messageBuilder.appendSizeIfDefined(entry.providedSize);
+ let dependencyName;
+ switch (infoType) {
+ case PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN:
+ dependencyName = 'the aggregated size of the children';
+ break;
+ case PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER:
+ dependencyName = 'the size of the largest owner';
+ break;
+ default:
+ dependencyName = 'an unknown dependency';
+ break;
+ }
+ messageBuilder.append(' was less than ', dependencyName);
+ messageBuilder.appendSizeIfDefined(entry.dependencySize);
+ if (entry.count < numerics.length) {
+ messageBuilder.appendSomeTimestampsQuantifier();
+ }
+ infos.push(tr.ui.analysis.createWarningInfo(messageBuilder.build()));
+ }
+ }
+ };
+
+ const NUMERIC_COLUMN_RULES = [
+ {
+ condition: tr.model.MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME,
+ importance: 10,
+ columnConstructor: EffectiveSizeColumn
+ },
+ {
+ condition: tr.model.MemoryAllocatorDump.SIZE_NUMERIC_NAME,
+ importance: 9,
+ columnConstructor: SizeColumn
+ },
+ {
+ condition: 'page_size',
+ importance: 0,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: /size/,
+ importance: 5,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ // All other columns.
+ importance: 0,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ }
+ ];
+
+ const DIAGNOSTIC_COLUMN_RULES = [
+ {
+ importance: 0,
+ columnConstructor: tr.ui.analysis.StringMemoryColumn
+ }
+ ];
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-allocator-details-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.memoryAllocatorDumps_ = undefined;
+ this.heapDumps_ = undefined;
+ this.aggregationMode_ = undefined;
+ },
+
+ ready() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ },
+
+ /**
+ * Sets the memory allocator dumps and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronological list of memory allocator
+ * dumps. All dumps are assumed to belong to the same process and have
+ * the same full name. Example:
+ *
+ * [
+ * tr.model.MemoryAllocatorDump {}, // MAD at timestamp 1.
+ * undefined, // MAD not provided at timestamp 2.
+ * tr.model.MemoryAllocatorDump {}, // MAD at timestamp 3.
+ * ]
+ */
+ set memoryAllocatorDumps(memoryAllocatorDumps) {
+ this.memoryAllocatorDumps_ = memoryAllocatorDumps;
+ this.scheduleRebuild_();
+ },
+
+ get memoryAllocatorDumps() {
+ return this.memoryAllocatorDumps_;
+ },
+
+ // TODO(petrcermak): Don't plumb the heap dumps through the allocator
+ // details pane. Maybe add support for multiple child panes to stacked pane
+ // (view) instead.
+ set heapDumps(heapDumps) {
+ this.heapDumps_ = heapDumps;
+ this.scheduleRebuild_();
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ onRebuild_() {
+ if (this.memoryAllocatorDumps_ === undefined ||
+ this.memoryAllocatorDumps_.length === 0) {
+ // Show the info text (hide the table).
+ this.$.info_text.style.display = 'block';
+ this.$.table.style.display = 'none';
+
+ this.$.table.clear();
+ this.$.table.rebuild();
+
+ // Hide the heap details pane (if applicable).
+ this.childPaneBuilder = undefined;
+ return;
+ }
+
+ // Show the table (hide the info text).
+ this.$.info_text.style.display = 'none';
+ this.$.table.style.display = 'block';
+
+ const rows = this.createRows_();
+ const columns = this.createColumns_(rows);
+ rows.forEach(function(rootRow) {
+ tr.ui.analysis.aggregateTableRowCellsRecursively(rootRow, columns,
+ function(contexts) {
+ // Only aggregate suballocation rows (numerics of regular rows
+ // corresponding to MADs have already been aggregated by the
+ // model in MemoryAllocatorDump.aggregateNumericsRecursively).
+ return contexts !== undefined && contexts.some(function(context) {
+ return context === SUBALLOCATION_CONTEXT;
+ });
+ });
+ });
+
+ this.$.table.tableRows = rows;
+ this.$.table.tableColumns = columns;
+ this.$.table.rebuild();
+ tr.ui.analysis.expandTableRowsRecursively(this.$.table);
+
+ // Show/hide the heap details pane.
+ if (this.heapDumps_ === undefined) {
+ this.childPaneBuilder = undefined;
+ } else {
+ this.childPaneBuilder = function() {
+ const pane =
+ document.createElement('tr-ui-a-memory-dump-heap-details-pane');
+ pane.heapDumps = this.heapDumps_;
+ pane.aggregationMode = this.aggregationMode_;
+ return pane;
+ }.bind(this);
+ }
+ },
+
+ createRows_() {
+ return [
+ this.createAllocatorRowRecursively_(this.memoryAllocatorDumps_)
+ ];
+ },
+
+ createAllocatorRowRecursively_(dumps) {
+ // Get the name of the memory allocator dumps. We can use any defined
+ // dump in dumps since they all have the same name.
+ const definedDump = dumps.find(x => x);
+ const title = definedDump.name;
+ const fullName = definedDump.fullName;
+
+ // Transform a chronological list of memory allocator dumps into two
+ // dictionaries of cells (where each cell contains a chronological list
+ // of the values of one of its numerics or diagnostics).
+ const numericCells = tr.ui.analysis.createCells(dumps, function(dump) {
+ return dump.numerics;
+ });
+ const diagnosticCells = tr.ui.analysis.createCells(dumps, function(dump) {
+ return dump.diagnostics;
+ });
+
+ // Determine whether the memory allocator dump is a suballocation. A
+ // dump is assumed to be a suballocation if (1) its name starts with
+ // two underscores, (2) it has an owner from within the same process at
+ // some timestamp, and (3) it is undefined, has no owners, or has the
+ // same owner (and no other owners) at all other timestamps.
+ let suballocatedBy = undefined;
+ if (title.startsWith('__')) {
+ for (let i = 0; i < dumps.length; i++) {
+ const dump = dumps[i];
+ if (dump === undefined || dump.ownedBy.length === 0) {
+ // Ignore timestamps where the dump is undefined or doesn't
+ // have any owner.
+ continue;
+ }
+ const ownerDump = dump.ownedBy[0].source;
+ if (dump.ownedBy.length > 1 ||
+ dump.children.length > 0 ||
+ ownerDump.containerMemoryDump !== dump.containerMemoryDump) {
+ // If the dump has (1) any children, (2) multiple owners, or
+ // (3) its owner is in a different process (otherwise, the
+ // modified title would be ambiguous), then it's not considered
+ // to be a suballocation.
+ suballocatedBy = undefined;
+ break;
+ }
+ if (suballocatedBy === undefined) {
+ suballocatedBy = ownerDump.fullName;
+ } else if (suballocatedBy !== ownerDump.fullName) {
+ // The full name of the owner dump changed over time, so this
+ // dump is not a suballocation.
+ suballocatedBy = undefined;
+ break;
+ }
+ }
+ }
+
+ const row = {
+ title,
+ fullNames: [fullName],
+ contexts: dumps,
+ numericCells,
+ diagnosticCells,
+ suballocatedBy
+ };
+
+ // Child memory dump name (dict key) -> Timestamp (list index) ->
+ // Child dump.
+ const childDumpNameToDumps = tr.b.invertArrayOfDicts(dumps,
+ function(dump) {
+ const results = {};
+ for (const child of dump.children) {
+ results[child.name] = child;
+ }
+ return results;
+ });
+
+ // Recursively create sub-rows for children (if applicable).
+ const subRows = [];
+ let suballocationClassificationRootNode = undefined;
+ for (const childDumps of Object.values(childDumpNameToDumps)) {
+ const childRow = this.createAllocatorRowRecursively_(childDumps);
+ if (childRow.suballocatedBy === undefined) {
+ // Not a suballocation row: just append it.
+ subRows.push(childRow);
+ } else {
+ // Suballocation row: classify it in a tree of suballocations.
+ suballocationClassificationRootNode =
+ this.classifySuballocationRow_(
+ childRow, suballocationClassificationRootNode);
+ }
+ }
+
+ // Build the tree of suballocations (if applicable).
+ if (suballocationClassificationRootNode !== undefined) {
+ const suballocationRow = this.createSuballocationRowRecursively_(
+ 'suballocations', suballocationClassificationRootNode);
+ subRows.push(suballocationRow);
+ }
+
+ if (subRows.length > 0) {
+ row.subRows = subRows;
+ }
+
+ return row;
+ },
+
+ classifySuballocationRow_(suballocationRow, rootNode) {
+ if (rootNode === undefined) {
+ rootNode = {
+ children: {},
+ row: undefined
+ };
+ }
+
+ const suballocationLevels = suballocationRow.suballocatedBy.split('/');
+ let currentNode = rootNode;
+ for (let i = 0; i < suballocationLevels.length; i++) {
+ const suballocationLevel = suballocationLevels[i];
+ let nextNode = currentNode.children[suballocationLevel];
+ if (nextNode === undefined) {
+ currentNode.children[suballocationLevel] = nextNode = {
+ children: {},
+ row: undefined
+ };
+ }
+ currentNode = nextNode;
+ }
+
+ const existingRow = currentNode.row;
+ if (existingRow !== undefined) {
+ // On rare occasions it can happen that one dump (e.g. sqlite) owns
+ // different suballocations at different timestamps (e.g.
+ // malloc/allocated_objects/_7d35 and malloc/allocated_objects/_511e).
+ // When this happens, we merge the two suballocations into a single row
+ // (malloc/allocated_objects/suballocations/sqlite).
+ for (let i = 0; i < suballocationRow.contexts.length; i++) {
+ const newContext = suballocationRow.contexts[i];
+ if (newContext === undefined) continue;
+
+ if (existingRow.contexts[i] !== undefined) {
+ throw new Error('Multiple suballocations with the same owner name');
+ }
+
+ existingRow.contexts[i] = newContext;
+ ['numericCells', 'diagnosticCells'].forEach(function(cellKey) {
+ const suballocationCells = suballocationRow[cellKey];
+ if (suballocationCells === undefined) return;
+ for (const [cellName, cell] of Object.entries(suballocationCells)) {
+ if (cell === undefined) continue;
+ const fields = cell.fields;
+ if (fields === undefined) continue;
+ const field = fields[i];
+ if (field === undefined) continue;
+ let existingCells = existingRow[cellKey];
+ if (existingCells === undefined) {
+ existingCells = {};
+ existingRow[cellKey] = existingCells;
+ }
+ let existingCell = existingCells[cellName];
+ if (existingCell === undefined) {
+ existingCell = new tr.ui.analysis.MemoryCell(
+ new Array(fields.length));
+ existingCells[cellName] = existingCell;
+ }
+ existingCell.fields[i] = field;
+ }
+ });
+ }
+ existingRow.fullNames.push.apply(
+ existingRow.fullNames, suballocationRow.fullNames);
+ } else {
+ currentNode.row = suballocationRow;
+ }
+
+ return rootNode;
+ },
+
+ createSuballocationRowRecursively_(name, node) {
+ const childCount = Object.keys(node.children).length;
+ if (childCount === 0) {
+ if (node.row === undefined) {
+ throw new Error('Suballocation node must have a row or children');
+ }
+ // Leaf row of the suballocation tree: Change the row's title from
+ // '__MEANINGLESSHASH' to the name of the suballocation owner.
+ const row = node.row;
+ row.title = name;
+ row.suballocation = true;
+ return row;
+ }
+
+ // Internal row of the suballocation tree: Recursively create its
+ // sub-rows.
+ const subRows = [];
+ for (const [subName, subNode] of Object.entries(node.children)) {
+ subRows.push(this.createSuballocationRowRecursively_(subName, subNode));
+ }
+
+ if (node.row !== undefined) {
+ // Very unlikely case: Both an ancestor (e.g. 'skia') and one of its
+ // descendants (e.g. 'skia/sk_glyph_cache') both suballocate from the
+ // same MemoryAllocatorDump (e.g. 'malloc/allocated_objects'). In
+ // this case, the suballocation from the ancestor must be mapped to
+ // 'malloc/allocated_objects/suballocations/skia/<unspecified>' so
+ // that 'malloc/allocated_objects/suballocations/skia' could
+ // aggregate the numerics of the two suballocations properly.
+ const row = node.row;
+ row.title = '<unspecified>';
+ row.suballocation = true;
+ subRows.unshift(row);
+ }
+
+ // An internal row of the suballocation tree is assumed to be defined
+ // at a given timestamp if at least one of its sub-rows is defined at
+ // the timestamp.
+ const contexts = new Array(subRows[0].contexts.length);
+ for (let i = 0; i < subRows.length; i++) {
+ subRows[i].contexts.forEach(function(subContext, index) {
+ if (subContext !== undefined) {
+ contexts[index] = SUBALLOCATION_CONTEXT;
+ }
+ });
+ }
+
+ return {
+ title: name,
+ suballocation: true,
+ contexts,
+ subRows
+ };
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new AllocatorDumpNameColumn();
+ titleColumn.width = '200px';
+
+ const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'numericCells',
+ aggregationMode: this.aggregationMode_,
+ rules: NUMERIC_COLUMN_RULES
+ });
+ const diagnosticColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'diagnosticCells',
+ aggregationMode: this.aggregationMode_,
+ rules: DIAGNOSTIC_COLUMN_RULES
+ });
+ const fieldColumns = numericColumns.concat(diagnosticColumns);
+ tr.ui.analysis.MemoryColumn.spaceEqually(fieldColumns);
+
+ const columns = [titleColumn].concat(fieldColumns);
+ return columns;
+ }
+ });
+
+ return {
+ // All exports are for testing only.
+ SUBALLOCATION_CONTEXT,
+ AllocatorDumpNameColumn,
+ EffectiveSizeColumn,
+ SizeColumn,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html
new file mode 100644
index 00000000000..6fda765b34b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_allocator_details_pane_test.html
@@ -0,0 +1,1261 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/heap_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_allocator_details_pane.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const MemoryAllocatorDump = tr.model.MemoryAllocatorDump;
+ const Scalar = tr.b.Scalar;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const HeapDump = tr.model.HeapDump;
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const checkNumericFields = tr.ui.analysis.checkNumericFields;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const checkStringFields = tr.ui.analysis.checkStringFields;
+ const checkColumnInfosAndColor = tr.ui.analysis.checkColumnInfosAndColor;
+ const checkColumns = tr.ui.analysis.checkColumns;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+ const AllocatorDumpNameColumn = tr.ui.analysis.AllocatorDumpNameColumn;
+ const EffectiveSizeColumn = tr.ui.analysis.EffectiveSizeColumn;
+ const SizeColumn = tr.ui.analysis.SizeColumn;
+ const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn;
+ const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+
+ const SUBALLOCATION_CONTEXT = tr.ui.analysis.SUBALLOCATION_CONTEXT;
+ const MemoryAllocatorDumpInfoType = tr.model.MemoryAllocatorDumpInfoType;
+ const PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN =
+ MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN;
+ const PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER =
+ MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER;
+
+ function addRootDumps(containerMemoryDump, rootNames, addedCallback) {
+ // Test sanity check.
+ assert.isUndefined(containerMemoryDump.memoryAllocatorDumps);
+
+ const rootDumps = rootNames.map(function(rootName) {
+ return new MemoryAllocatorDump(containerMemoryDump, rootName);
+ });
+ addedCallback.apply(null, rootDumps);
+ containerMemoryDump.memoryAllocatorDumps = rootDumps;
+ }
+
+ function newSuballocationDump(ownerDump, parentDump, name, size) {
+ const suballocationDump = addChildDump(parentDump, name,
+ {numerics: {size}});
+ if (ownerDump !== undefined) {
+ addOwnershipLink(ownerDump, suballocationDump);
+ }
+ return suballocationDump;
+ }
+
+ function createProcessMemoryDumps() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+
+ // First timestamp.
+ const gmd1 = addGlobalMemoryDump(model, {ts: -10});
+ const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11});
+ pmd1.memoryAllocatorDumps = (function() {
+ const v8Dump = newAllocatorDump(pmd1, 'v8', {numerics: {
+ size: 1073741824 /* 1 GiB */,
+ inner_size: 2097152 /* 2 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204)
+ }});
+
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 805306368 /* 768 MiB */}});
+ addChildDump(v8HeapsDump, 'heap42',
+ {numerics: {size: 804782080 /* 767.5 MiB */}});
+
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects');
+ v8ObjectsDump.addDiagnostic('url', 'http://example.com');
+ addChildDump(v8ObjectsDump, 'foo',
+ {numerics: {size: 1022976 /* 999 KiB */}});
+ addChildDump(v8ObjectsDump, 'bar',
+ {numerics: {size: 1024000 /* 1000 KiB */}});
+
+ const oilpanDump = newAllocatorDump(pmd1, 'oilpan',
+ {numerics: {size: 125829120 /* 120 MiB */}});
+ newSuballocationDump(
+ oilpanDump, v8Dump, '__99BEAD', 150994944 /* 144 MiB */);
+
+ const oilpanSubDump = addChildDump(oilpanDump, 'animals');
+
+ const oilpanSubDump1 = addChildDump(oilpanSubDump, 'cow',
+ {numerics: {size: 33554432 /* 32 MiB */}});
+ newSuballocationDump(
+ oilpanSubDump1, v8Dump, '__42BEEF', 67108864 /* 64 MiB */);
+
+ const oilpanSubDump2 = addChildDump(oilpanSubDump, 'chicken',
+ {numerics: {size: 16777216 /* 16 MiB */}});
+ newSuballocationDump(
+ oilpanSubDump2, v8Dump, '__68DEAD', 33554432 /* 32 MiB */);
+
+ const skiaDump = newAllocatorDump(pmd1, 'skia',
+ {numerics: {size: 8388608 /* 8 MiB */}});
+ const suballocationDump = newSuballocationDump(
+ skiaDump, v8Dump, '__15FADE', 16777216 /* 16 MiB */);
+
+ const ccDump = newAllocatorDump(pmd1, 'cc',
+ {numerics: {size: 4194304 /* 4 MiB */}});
+ newSuballocationDump(
+ ccDump, v8Dump, '__12FEED', 5242880 /* 5 MiB */).addDiagnostic(
+ 'url', 'localhost:1234');
+
+ return [v8Dump, oilpanDump, skiaDump, ccDump];
+ })();
+
+ // Second timestamp.
+ const gmd2 = addGlobalMemoryDump(model, {ts: 10});
+ const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 11});
+ pmd2.memoryAllocatorDumps = (function() {
+ const v8Dump = newAllocatorDump(pmd2, 'v8', {numerics: {
+ size: 1073741824 /* 1 GiB */,
+ inner_size: 2097152 /* 2 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204)
+ }});
+
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects');
+ v8ObjectsDump.addDiagnostic('url', 'http://sample.net');
+ addChildDump(v8ObjectsDump, 'foo',
+ {numerics: {size: 1020928 /* 997 KiB */}});
+ addChildDump(v8ObjectsDump, 'bar',
+ {numerics: {size: 1026048 /* 1002 KiB */}});
+
+ newSuballocationDump(
+ undefined, v8Dump, '__99BEAD', 268435456 /* 256 MiB */);
+
+ const ccDump = newAllocatorDump(pmd2, 'cc',
+ {numerics: {size: 7340032 /* 7 MiB */}});
+ newSuballocationDump(
+ ccDump, v8Dump, '__13DEED', 11534336 /* 11 MiB */).addDiagnostic(
+ 'url', 'localhost:5678');
+
+ return [v8Dump, ccDump];
+ })();
+ });
+
+ return model.processes[1].memoryDumps;
+ }
+
+ function createSizeFields(values) {
+ return values.map(function(value) {
+ if (value === undefined) return undefined;
+ return new Scalar(sizeInBytes_smallerIsBetter, value);
+ });
+ }
+
+ const EXPECTED_COLUMNS = [
+ { title: 'Component', type: AllocatorDumpNameColumn, noAggregation: true },
+ { title: 'effective_size', type: EffectiveSizeColumn },
+ { title: 'size', type: SizeColumn },
+ { title: 'inner_size', type: NumericMemoryColumn },
+ { title: 'objects_count', type: NumericMemoryColumn },
+ { title: 'url', type: StringMemoryColumn }
+ ];
+
+ function checkRow(columns, row, expectations) {
+ const formattedTitle = columns[0].formatTitle(row);
+ const expectedTitle = expectations.title;
+ if (typeof expectedTitle === 'function') {
+ expectedTitle(formattedTitle);
+ } else {
+ assert.strictEqual(formattedTitle, expectedTitle);
+ }
+
+ checkSizeNumericFields(row, columns[1], expectations.size);
+ checkSizeNumericFields(row, columns[2], expectations.effective_size);
+ checkSizeNumericFields(row, columns[3], expectations.inner_size);
+ checkNumericFields(row, columns[4], expectations.objects_count,
+ unitlessNumber_smallerIsBetter);
+ checkStringFields(row, columns[5], expectations.url);
+
+ const expectedSubRowCount = expectations.sub_row_count;
+ if (expectedSubRowCount === undefined) {
+ assert.isUndefined(row.subRows);
+ } else {
+ assert.lengthOf(row.subRows, expectedSubRowCount);
+ }
+
+ const expectedContexts = expectations.contexts;
+ if (expectedContexts === undefined) {
+ assert.isUndefined(row.contexts);
+ } else {
+ assert.deepEqual(Array.from(row.contexts), expectedContexts);
+ }
+ }
+
+ function buildProcessMemoryDumps(count, preFinalizeDumpsCallback) {
+ const pmds = new Array(count);
+ tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < count; i++) {
+ const timestamp = 10 + i;
+ const gmd = addGlobalMemoryDump(model, {ts: timestamp});
+ pmds[i] = addProcessMemoryDump(gmd, process, {ts: timestamp});
+ }
+ preFinalizeDumpsCallback(pmds);
+ });
+ return pmds;
+ }
+
+ function getAllocatorDumps(pmds, fullName) {
+ return pmds.map(function(pmd) {
+ if (pmd === undefined) return undefined;
+ return pmd.getMemoryAllocatorDumpByFullName(fullName);
+ });
+ }
+
+ function checkAllocatorPaneColumnInfosAndColor(
+ column, dumps, numericName, expectedInfos) {
+ const numerics = dumps.map(function(dump) {
+ if (dump === undefined) return undefined;
+ return dump.numerics[numericName];
+ });
+ checkColumnInfosAndColor(
+ column, numerics, dumps, expectedInfos, undefined /* no color */);
+ }
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-allocator-details-pane', 'memoryAllocatorDumps',
+ function(viewEl) {
+ // Check that the info text is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.info_text));
+ assert.isFalse(isElementDisplayed(viewEl.$.table));
+ });
+ });
+
+ test('instantiate_single', function() {
+ const processMemoryDumps = createProcessMemoryDumps().slice(0, 1);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.rebuild();
+ assert.deepEqual(viewEl.requestedChildPanes, [undefined]);
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, undefined /* no aggregation */);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check the rows of the table.
+ const rootRow = rows[0];
+ checkRow(columns, rootRow, {
+ title: 'v8',
+ size: [942619648],
+ effective_size: [1081031680],
+ inner_size: [2097152],
+ objects_count: [204],
+ sub_row_count: 3,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8'),
+ });
+
+ const heapsSubRow = rootRow.subRows[0];
+ checkRow(columns, heapsSubRow, {
+ title: 'heaps',
+ size: [805306368],
+ effective_size: [805306368],
+ sub_row_count: 2,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps'),
+ });
+
+ const heapsUnspecifiedSubRow = heapsSubRow.subRows[0];
+ checkRow(columns, heapsUnspecifiedSubRow, {
+ title: '<unspecified>',
+ size: [524288],
+ effective_size: [524288],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps/<unspecified>'),
+ });
+
+ const suballocationsSubRow = rootRow.subRows[2];
+ checkRow(columns, suballocationsSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, 'suballocations');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [135266304],
+ effective_size: [273678336],
+ sub_row_count: 3,
+ contexts: [SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanSuballocationSubRow = suballocationsSubRow.subRows[0];
+ checkRow(columns, oilpanSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'oilpan');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [125829120],
+ effective_size: [251658240],
+ sub_row_count: 2,
+ contexts: [SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanUnspecifiedSuballocationSubRow =
+ oilpanSuballocationSubRow.subRows[0];
+ checkRow(columns, oilpanUnspecifiedSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, '<unspecified>');
+ assert.strictEqual(formattedTitle.title, 'v8/__99BEAD');
+ },
+ size: [75497472],
+ effective_size: [150994944],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__99BEAD'),
+ });
+
+ const oilpanAnimalsSuballocationSubRow =
+ oilpanSuballocationSubRow.subRows[1];
+ checkRow(columns, oilpanAnimalsSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'animals');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [50331648],
+ effective_size: [100663296],
+ sub_row_count: 2,
+ contexts: [SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanCowSuballocationSubRow =
+ oilpanAnimalsSuballocationSubRow.subRows[0];
+ checkRow(columns, oilpanCowSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cow');
+ assert.strictEqual(formattedTitle.title, 'v8/__42BEEF');
+ },
+ size: [33554432],
+ effective_size: [67108864],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__42BEEF'),
+ });
+
+ const skiaSuballocationSubRow = suballocationsSubRow.subRows[1];
+ checkRow(columns, skiaSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'skia');
+ assert.strictEqual(formattedTitle.title, 'v8/__15FADE');
+ },
+ size: [8388608],
+ effective_size: [16777216],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__15FADE'),
+ });
+
+ const ccSuballocationSubRow = suballocationsSubRow.subRows[2];
+ checkRow(columns, ccSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cc');
+ assert.strictEqual(formattedTitle.title, 'v8/__12FEED');
+ },
+ size: [1048576],
+ effective_size: [5242880],
+ url: ['localhost:1234'],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__12FEED')
+ });
+ });
+
+ test('instantiate_multipleDiff', function() {
+ const processMemoryDumps = createProcessMemoryDumps();
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ assert.deepEqual(viewEl.requestedChildPanes, [undefined]);
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check the rows of the table.
+ const rootRow = rows[0];
+ checkRow(columns, rootRow, {
+ title: 'v8',
+ size: [942619648, 1066401792],
+ effective_size: [1081031680, 1073741824],
+ inner_size: [2097152, 2097152],
+ objects_count: [204, 204],
+ sub_row_count: 4,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8'),
+ });
+
+ const heapsSubRow = rootRow.subRows[0];
+ checkRow(columns, heapsSubRow, {
+ title: 'heaps',
+ size: [805306368, undefined],
+ effective_size: [805306368, undefined],
+ sub_row_count: 2,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps'),
+ });
+
+ const heapsUnspecifiedSubRow = heapsSubRow.subRows[0];
+ checkRow(columns, heapsUnspecifiedSubRow, {
+ title: '<unspecified>',
+ size: [524288, undefined],
+ effective_size: [524288, undefined],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/heaps/<unspecified>'),
+ });
+
+ const unspecifiedSubRow = rootRow.subRows[2];
+ checkRow(columns, unspecifiedSubRow, {
+ title: '<unspecified>',
+ size: [undefined, 791725056],
+ effective_size: [undefined, 791725056],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/<unspecified>'),
+ });
+
+ const suballocationsSubRow = rootRow.subRows[3];
+ checkRow(columns, suballocationsSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, 'suballocations');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [135266304, 272629760],
+ effective_size: [273678336, 279969792],
+ sub_row_count: 3,
+ contexts: [SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanSuballocationSubRow = suballocationsSubRow.subRows[0];
+ checkRow(columns, oilpanSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'oilpan');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [125829120, 268435456],
+ effective_size: [251658240, 268435456],
+ sub_row_count: 2,
+ contexts: [SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT],
+ });
+
+ const oilpanUnspecifiedSuballocationSubRow =
+ oilpanSuballocationSubRow.subRows[0];
+ checkRow(columns, oilpanUnspecifiedSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, '<unspecified>');
+ assert.strictEqual(formattedTitle.title, 'v8/__99BEAD');
+ },
+ size: [75497472, 268435456],
+ effective_size: [150994944, 268435456],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__99BEAD'),
+ });
+
+ const oilpanAnimalsSuballocationSubRow =
+ oilpanSuballocationSubRow.subRows[1];
+ checkRow(columns, oilpanAnimalsSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'animals');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [50331648, undefined],
+ effective_size: [100663296, undefined],
+ sub_row_count: 2,
+ contexts: [SUBALLOCATION_CONTEXT, undefined],
+ });
+
+ const oilpanCowSuballocationSubRow =
+ oilpanAnimalsSuballocationSubRow.subRows[0];
+ checkRow(columns, oilpanCowSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cow');
+ assert.strictEqual(formattedTitle.title, 'v8/__42BEEF');
+ },
+ size: [33554432, undefined],
+ effective_size: [67108864, undefined],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__42BEEF'),
+ });
+
+ const skiaSuballocationSubRow = suballocationsSubRow.subRows[1];
+ checkRow(columns, skiaSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'skia');
+ assert.strictEqual(formattedTitle.title, 'v8/__15FADE');
+ },
+ size: [8388608, undefined],
+ effective_size: [16777216, undefined],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/__15FADE'),
+ });
+
+ const ccSuballocationSubRow = suballocationsSubRow.subRows[2];
+ checkRow(columns, ccSuballocationSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(Polymer.dom(formattedTitle).textContent, 'cc');
+ assert.strictEqual(formattedTitle.title, 'v8/__12FEED, v8/__13DEED');
+ },
+ size: [1048576, 4194304],
+ effective_size: [5242880, 11534336],
+ url: ['localhost:1234', 'localhost:5678'],
+ contexts: [
+ processMemoryDumps[0].getMemoryAllocatorDumpByFullName('v8/__12FEED'),
+ processMemoryDumps[1].getMemoryAllocatorDumpByFullName('v8/__13DEED')
+ ]
+ });
+ });
+
+ test('instantiate_multipleMax', function() {
+ const processMemoryDumps = createProcessMemoryDumps();
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.aggregationMode = AggregationMode.MAX;
+ viewEl.rebuild();
+ assert.deepEqual(viewEl.requestedChildPanes, [undefined]);
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ // Just check that the aggregation mode was propagated to the columns.
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.MAX);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+ });
+
+ test('instantiate_multipleWithUndefined', function() {
+ const processMemoryDumps = createProcessMemoryDumps();
+ processMemoryDumps.splice(1, 0, undefined);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ assert.deepEqual(viewEl.requestedChildPanes, [undefined]);
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check only a few rows of the table.
+ const rootRow = rows[0];
+ checkRow(columns, rootRow, {
+ title: 'v8',
+ size: [942619648, undefined, 1066401792],
+ effective_size: [1081031680, undefined, 1073741824],
+ inner_size: [2097152, undefined, 2097152],
+ objects_count: [204, undefined, 204],
+ sub_row_count: 4,
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8'),
+ });
+
+ const unspecifiedSubRow = rootRow.subRows[2];
+ checkRow(columns, unspecifiedSubRow, {
+ title: '<unspecified>',
+ size: [undefined, undefined, 791725056],
+ effective_size: [undefined, undefined, 791725056],
+ contexts: getAllocatorDumps(processMemoryDumps, 'v8/<unspecified>'),
+ });
+
+ const suballocationsSubRow = rootRow.subRows[3];
+ checkRow(columns, suballocationsSubRow, {
+ title(formattedTitle) {
+ assert.strictEqual(
+ Polymer.dom(formattedTitle).textContent, 'suballocations');
+ assert.strictEqual(formattedTitle.title, '');
+ },
+ size: [135266304, undefined, 272629760],
+ effective_size: [273678336, undefined, 279969792],
+ sub_row_count: 3,
+ contexts: [SUBALLOCATION_CONTEXT, undefined, SUBALLOCATION_CONTEXT],
+ });
+ });
+
+ test('heapDumpsPassThrough', function() {
+ const processMemoryDumps = createProcessMemoryDumps();
+ const heapDumps = processMemoryDumps.map(function(dump) {
+ if (dump === undefined) return undefined;
+ return new HeapDump(dump, 'v8');
+ });
+
+ // Start by creating a component details pane without any heap dumps.
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ viewEl.memoryAllocatorDumps = getAllocatorDumps(processMemoryDumps, 'v8');
+ viewEl.aggregationMode = AggregationMode.MAX;
+ viewEl.rebuild();
+
+ assert.lengthOf(viewEl.requestedChildPanes, 1);
+ assert.isUndefined(viewEl.requestedChildPanes[0]);
+
+ // Set the heap dumps. This should trigger creating a heap details pane.
+ viewEl.heapDumps = heapDumps;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+
+ assert.lengthOf(viewEl.requestedChildPanes, 2);
+ assert.strictEqual(viewEl.requestedChildPanes[1].tagName,
+ 'TR-UI-A-MEMORY-DUMP-HEAP-DETAILS-PANE');
+ assert.strictEqual(viewEl.requestedChildPanes[1].heapDumps, heapDumps);
+ assert.strictEqual(viewEl.requestedChildPanes[1].aggregationMode,
+ AggregationMode.DIFF);
+
+ // Unset the heap dumps. This should trigger removing the heap details pane.
+ viewEl.heapDumps = undefined;
+ viewEl.rebuild();
+
+ assert.lengthOf(viewEl.requestedChildPanes, 3);
+ assert.isUndefined(viewEl.requestedChildPanes[2]);
+ });
+
+ test('allocatorDumpNameColumn', function() {
+ const c = new AllocatorDumpNameColumn();
+
+ // Regular row.
+ assert.strictEqual(c.formatTitle({title: 'Regular row'}), 'Regular row');
+
+ // Sub-allocation row.
+ const row = c.formatTitle({
+ title: 'Suballocation row',
+ suballocation: true,
+ });
+ assert.strictEqual(Polymer.dom(row).textContent, 'Suballocation row');
+ assert.strictEqual(row.style.fontStyle, 'italic');
+ });
+
+ test('effectiveSizeColumn_noContext', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.DIFF);
+
+ // Single selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128]),
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // Multi-selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128, 256, undefined, 64]),
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+ });
+
+ test('effectiveSizeColumn_suballocationContext', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.MAX);
+
+ // Single selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128]),
+ [SUBALLOCATION_CONTEXT],
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // Multi-selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([undefined, 256, undefined, 64]),
+ [undefined, SUBALLOCATION_CONTEXT, SUBALLOCATION_CONTEXT,
+ SUBALLOCATION_CONTEXT],
+ [] /* no infos */,
+ undefined /* no color */);
+ });
+
+ test('effectiveSizeColumn_dumpContext_noOwnership', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ const pmds = buildProcessMemoryDumps(4 /* count */, function(pmds) {
+ addRootDumps(pmds[0], ['v8'], function(v8Dump) {
+ addChildDump(v8Dump, 'heaps', {numerics: {size: 64}});
+ });
+ addRootDumps(pmds[2], ['v8'], function(v8Dump) {
+ addChildDump(v8Dump, 'heaps', {numerics: {size: 128}});
+ });
+ addRootDumps(pmds[3], ['v8'], function(v8Dump) {});
+ });
+ const v8HeapsDumps = getAllocatorDumps(pmds, 'v8/heaps');
+
+ // Single selection.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDumps[0]],
+ 'effective_size',
+ [] /* no infos */);
+
+ // Multi-selection, all dumps defined.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDumps[0], v8HeapsDumps[2]],
+ 'effective_size',
+ [] /* no infos */);
+
+ // Multi-selection, some dumps missing.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ v8HeapsDumps,
+ 'effective_size',
+ [] /* no infos */);
+ });
+
+ test('effectiveSizeColumn_dumpContext_singleOwnership', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.MAX);
+ const pmds = buildProcessMemoryDumps(5 /* count */, function(pmds) {
+ addRootDumps(pmds[0], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump);
+ });
+ addRootDumps(pmds[1], ['v8'], function(v8Dump) {
+ addChildDump(v8Dump, 'heaps', {numerics: {size: 32}});
+ // Missing oilpan/objects dump.
+ });
+ addRootDumps(pmds[2], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ // Missing v8/heaps dump.
+ });
+ addRootDumps(pmds[3], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ addChildDump(v8Dump, 'heaps', {numerics: {size: 32}});
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ // Missing ownership link.
+ });
+ addRootDumps(pmds[4], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 2);
+ });
+ });
+ const v8HeapsDump = getAllocatorDumps(pmds, 'v8/heaps');
+ const oilpanObjectsDump = getAllocatorDumps(pmds, 'oilpan/objects');
+
+ // Single selection.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDump[0]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'oilpan/objects\' in Process 1 (importance: 0) ' +
+ 'with no other dumps',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [oilpanObjectsDump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by \'v8/heaps\' in Process 1 (importance: 2)',
+ color: 'green'
+ }
+ ]);
+
+ // Multi-selection, all dumps defined.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDump[0], v8HeapsDump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'oilpan/objects\' in Process 1 (importance: ' +
+ '0\u20132) with no other dumps',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [oilpanObjectsDump[0], oilpanObjectsDump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by \'v8/heaps\' in Process 1 (importance: ' +
+ '0\u20132)',
+ color: 'green'
+ }
+ ]);
+
+ // Multi-selection, some dumps missing.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ v8HeapsDump,
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'oilpan/objects\' in Process 1 at some ' +
+ 'selected timestamps (importance: 0\u20132) with no other ' +
+ 'dumps',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ oilpanObjectsDump,
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by \'v8/heaps\' in Process 1 at some selected ' +
+ 'timestamps (importance: 0\u20132)',
+ color: 'green'
+ }
+ ]);
+ });
+
+ test('effectiveSizeColumn_dumpContext_multipleOwnerships', function() {
+ const c = new EffectiveSizeColumn('Effective Size', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ const pmds = buildProcessMemoryDumps(6 /* count */, function(pmds) {
+ addRootDumps(pmds[0], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const v8QueuesDump = addChildDump(v8Dump, 'queues',
+ {numerics: {size: 8}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump);
+ addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1);
+ });
+ addRootDumps(pmds[1], ['v8'], function(v8Dump) {});
+ addRootDumps(pmds[2], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const v8QueuesDump = addChildDump(v8Dump, 'queues',
+ {numerics: {size: 8}});
+ const v8PilesDump = addChildDump(v8Dump, 'piles',
+ {numerics: {size: 48}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 2);
+ addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1);
+ addOwnershipLink(v8PilesDump, oilpanObjectsDump);
+ });
+ addRootDumps(pmds[3], ['v8', 'blink'], function(v8Dump, blinkDump) {
+ const blinkHandlesDump = addChildDump(blinkDump, 'handles',
+ {numerics: {size: 32}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 64}});
+ const blinkObjectsDump = addChildDump(blinkDump, 'objects',
+ {numerics: {size: 32}});
+ addOwnershipLink(blinkHandlesDump, v8HeapsDump, -273);
+ addOwnershipLink(v8HeapsDump, blinkObjectsDump, 3);
+ });
+ addRootDumps(pmds[4], ['v8', 'gpu'], function(v8Dump, gpuDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 64}});
+ const gpuTile1Dump = addChildDump(gpuDump, 'tile1',
+ {numerics: {size: 100}});
+ const gpuTile2Dump = addChildDump(gpuDump, 'tile2',
+ {numerics: {size: 99}});
+ addOwnershipLink(v8HeapsDump, gpuTile1Dump, 3);
+ addOwnershipLink(gpuTile2Dump, gpuTile1Dump, -1);
+ });
+ addRootDumps(pmds[5], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 32}});
+ const v8QueuesDump = addChildDump(v8Dump, 'queues',
+ {numerics: {size: 8}});
+ const v8PilesDump = addChildDump(v8Dump, 'piles',
+ {numerics: {size: 48}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 64}});
+ addOwnershipLink(v8HeapsDump, oilpanObjectsDump, 1);
+ addOwnershipLink(v8QueuesDump, oilpanObjectsDump, 1);
+ addOwnershipLink(v8PilesDump, oilpanObjectsDump, 7);
+ });
+ });
+ const v8HeapsDump = getAllocatorDumps(pmds, 'v8/heaps');
+ const oilpanObjectsDump = getAllocatorDumps(pmds, 'oilpan/objects');
+ const gpuTile1Dump = getAllocatorDumps(pmds, 'gpu/tile1');
+
+ // Single selection.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'gpu/tile1\' in Process 1 (importance: 3) with ' +
+ '\'gpu/tile2\' in Process 1 (importance: -1)',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [gpuTile1Dump[4]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by:\n' +
+ ' - \'v8/heaps\' in Process 1 (importance: 3)\n' +
+ ' - \'gpu/tile2\' in Process 1 (importance: -1)',
+ color: 'green'
+ }
+ ]);
+
+ // Multi-selection, all dumps defined.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapsDump[2], v8HeapsDump[5]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FE',
+ message: 'shares \'oilpan/objects\' in Process 1 (importance: ' +
+ '1\u20132) with:\n' +
+ ' - \'v8/queues\' in Process 1 (importance: 1)\n' +
+ ' - \'v8/piles\' in Process 1 (importance: 0\u20137)',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [oilpanObjectsDump[2], oilpanObjectsDump[5]],
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by:\n' +
+ ' - \'v8/heaps\' in Process 1 (importance: 1\u20132)\n' +
+ ' - \'v8/queues\' in Process 1 (importance: 1)\n' +
+ ' - \'v8/piles\' in Process 1 (importance: 0\u20137)',
+ color: 'green'
+ }
+ ]);
+
+ // Multi-selection, some dumps missing.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ v8HeapsDump,
+ 'effective_size',
+ [ // v8/objects is both owned (first info) and an owner (second info).
+ {
+ icon: '\u21FD',
+ message: 'shared by \'blink/handles\' in Process 1 at some ' +
+ 'selected timestamps (importance: -273)',
+ color: 'green'
+ },
+ {
+ icon: '\u21FE',
+ message: 'shares:\n' +
+ ' - \'oilpan/objects\' in Process 1 at some selected ' +
+ 'timestamps (importance: 0\u20132) with:\n' +
+ ' - \'v8/queues\' in Process 1 (importance: 1)\n' +
+ ' - \'v8/piles\' in Process 1 at some selected ' +
+ 'timestamps (importance: 0\u20137)\n' +
+ ' - \'blink/objects\' in Process 1 at some selected ' +
+ 'timestamps (importance: 3) with no other dumps\n' +
+ ' - \'gpu/tile1\' in Process 1 at some selected timestamps ' +
+ '(importance: 3) with \'gpu/tile2\' in Process 1 ' +
+ '(importance: -1)',
+ color: 'green'
+ }
+ ]);
+ checkAllocatorPaneColumnInfosAndColor(c,
+ oilpanObjectsDump,
+ 'effective_size',
+ [
+ {
+ icon: '\u21FD',
+ message: 'shared by:\n' +
+ ' - \'v8/heaps\' in Process 1 at some selected timestamps ' +
+ '(importance: 0\u20132)\n' +
+ ' - \'v8/queues\' in Process 1 at some selected timestamps ' +
+ '(importance: 1)\n' +
+ ' - \'v8/piles\' in Process 1 at some selected timestamps ' +
+ '(importance: 0\u20137)',
+ color: 'green'
+ }
+ ]);
+ });
+
+ test('sizeColumn_noContext', function() {
+ const c = new SizeColumn('Size', 'bytes', (x => x),
+ AggregationMode.DIFF);
+
+ // Single selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128]),
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // Multi-selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128, 256, undefined, 64]),
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+ });
+
+ test('sizeColumn_suballocationContext', function() {
+ const c = new SizeColumn('Size', 'bytes', (x => x),
+ AggregationMode.MAX);
+
+ // Single selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([128]),
+ [SUBALLOCATION_CONTEXT],
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // Multi-selection.
+ checkColumnInfosAndColor(c,
+ createSizeFields([undefined, 256, undefined, 64]),
+ [undefined, SUBALLOCATION_CONTEXT, undefined, SUBALLOCATION_CONTEXT],
+ [] /* no infos */,
+ undefined /* no color */);
+ });
+
+ test('sizeColumn_dumpContext', function() {
+ const c = new SizeColumn('Size', 'bytes', (x => x), AggregationMode.DIFF);
+ const pmds = buildProcessMemoryDumps(7 /* count */, function(pmds) {
+ addRootDumps(pmds[0], ['v8'], function(v8Dump) {
+ // Single direct overlap (v8/objects -> v8/heaps).
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects',
+ {numerics: {size: 1536}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ addOwnershipLink(v8ObjectsDump, v8HeapsDump);
+ });
+ // pmd[1] intentionally skipped.
+ addRootDumps(pmds[2], ['v8'], function(v8Dump, oilpanDump) {
+ // Single direct overlap with inconsistent owned dump size.
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects',
+ {numerics: {size: 3072}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ addOwnershipLink(v8ObjectsDump, v8HeapsDump);
+ });
+ addRootDumps(pmds[3], ['v8'], function(v8Dump) {
+ // Single indirect overlap (v8/objects/X -> v8/heaps/42).
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects',
+ {numerics: {size: 1536}});
+ const v8ObjectsXDump = addChildDump(v8ObjectsDump, 'X',
+ {numerics: {size: 512}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ const v8Heaps42Dump = addChildDump(v8HeapsDump, '42',
+ {numerics: {size: 1024}});
+ addOwnershipLink(v8ObjectsXDump, v8Heaps42Dump);
+ });
+ addRootDumps(pmds[4], ['v8'], function(v8Dump) {
+ // Multiple overlaps.
+ const v8ObjectsDump = addChildDump(v8Dump, 'objects',
+ {numerics: {size: 1024}});
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+
+ const v8ObjectsXDump = addChildDump(v8ObjectsDump, 'X',
+ {numerics: {size: 512}});
+ const v8Heaps42Dump = addChildDump(v8HeapsDump, '42',
+ {numerics: {size: 1280}});
+ addOwnershipLink(v8ObjectsXDump, v8Heaps42Dump);
+
+ const v8ObjectsYDump = addChildDump(v8ObjectsDump, 'Y',
+ {numerics: {size: 128}});
+ const v8Heaps90Dump = addChildDump(v8HeapsDump, '90',
+ {numerics: {size: 256}});
+ addOwnershipLink(v8ObjectsYDump, v8Heaps90Dump);
+
+ const v8BlocksDump = addChildDump(v8Dump, 'blocks',
+ {numerics: {size: 768}});
+ addOwnershipLink(v8BlocksDump, v8Heaps42Dump);
+ });
+ addRootDumps(pmds[5], ['v8'], function(v8Dump) {
+ // No overlaps, inconsistent parent size.
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ addChildDump(v8HeapsDump, '42', {numerics: {size: 1536}});
+ addChildDump(v8HeapsDump, '90', {numerics: {size: 615}});
+ });
+ addRootDumps(pmds[6], ['v8', 'oilpan'], function(v8Dump, oilpanDump) {
+ // No overlaps, inconsistent parent and owned dump size.
+ const v8HeapsDump = addChildDump(v8Dump, 'heaps',
+ {numerics: {size: 2048}});
+ addChildDump(v8HeapsDump, '42', {numerics: {size: 1536}});
+ addChildDump(v8HeapsDump, '90', {numerics: {size: 615}});
+ const oilpanObjectsDump =
+ addChildDump(oilpanDump, 'objects', {numerics: {size: 3072}});
+ addOwnershipLink(oilpanObjectsDump, v8HeapsDump);
+ });
+ });
+ const v8HeapDumps = getAllocatorDumps(pmds, 'v8/heaps');
+
+ // Single selection, single overlap.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[0]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its sibling \'objects\' (1.5 KiB)',
+ color: 'blue'
+ }
+ ]);
+
+ // Single selection, multiple overlaps.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[4]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its siblings:\n' +
+ ' - \'objects\' (640.0 B)\n' +
+ ' - \'blocks\' (768.0 B)',
+ color: 'blue'
+ }
+ ]);
+
+ // Single selection, warnings with no overlaps.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[6]],
+ 'size',
+ [
+ {
+ icon: '\u26A0',
+ message: 'provided size (2.0 KiB) was less than the aggregated ' +
+ 'size of the children (2.1 KiB)',
+ color: 'red'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size (2.0 KiB) was less than the size of the ' +
+ 'largest owner (3.0 KiB)',
+ color: 'red'
+ }
+ ]);
+
+ // Single selection, single overlap with a warning.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[2]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its sibling \'objects\' (3.0 KiB)',
+ color: 'blue'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size (2.0 KiB) was less than the size of the ' +
+ 'largest owner (3.0 KiB)',
+ color: 'red'
+ }
+ ]);
+
+ // Multi-selection, single overlap.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[0], v8HeapDumps[3]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its sibling \'objects\'',
+ color: 'blue'
+ }
+ ]);
+
+ // Multi-selection, multiple overlaps.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[0], v8HeapDumps[4]],
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its siblings:\n' +
+ ' - \'objects\'\n' +
+ ' - \'blocks\' at some selected timestamps',
+ color: 'blue'
+ }
+ ]);
+
+ // Multi-selection, warnings with no overlaps.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ [v8HeapDumps[5], v8HeapDumps[6]],
+ 'size',
+ [
+ {
+ icon: '\u26A0',
+ message: 'provided size was less than the aggregated ' +
+ 'size of the children',
+ color: 'red'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size was less than the size of the largest ' +
+ 'owner at some selected timestamps',
+ color: 'red'
+ }
+ ]);
+
+ // Multi-selection, multiple overlaps with warnings.
+ checkAllocatorPaneColumnInfosAndColor(c,
+ v8HeapDumps,
+ 'size',
+ [
+ {
+ icon: '\u24D8',
+ message: 'overlaps with its siblings:\n' +
+ ' - \'objects\' at some selected timestamps\n' +
+ ' - \'blocks\' at some selected timestamps',
+ color: 'blue'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size was less than the size of the largest ' +
+ 'owner at some selected timestamps',
+ color: 'red'
+ },
+ {
+ icon: '\u26A0',
+ message: 'provided size was less than the aggregated size of ' +
+ 'the children at some selected timestamps',
+ color: 'red'
+ }
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html
new file mode 100644
index 00000000000..1141116ec86
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_overview_pane.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<dom-module id='tr-ui-a-memory-dump-header-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ background-color: #d0d0d0;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ }
+
+ #label {
+ flex: 1 1 auto;
+ padding: 6px;
+ font-size: 15px;
+ }
+
+ #aggregation_mode_container {
+ display: none;
+ flex: 0 0 auto;
+ padding: 5px;
+ font-size: 15px;
+ }
+ </style>
+ </tr-ui-b-view-specific-brushing-state>
+ <div id="label"></div>
+ <div id="aggregation_mode_container">
+ <span>Metric aggregation:</span>
+ <!-- Aggregation mode selector (added in Polymer.ready()) -->
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ Polymer({
+ is: 'tr-ui-a-memory-dump-header-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.containerMemoryDumps_ = undefined;
+ },
+
+ ready() {
+ Polymer.dom(this.$.aggregation_mode_container).appendChild(
+ tr.ui.b.createSelector(this, 'aggregationMode',
+ 'memoryDumpHeaderPane.aggregationMode',
+ tr.ui.analysis.MemoryColumn.AggregationMode.DIFF, [
+ {
+ label: 'Diff',
+ value: tr.ui.analysis.MemoryColumn.AggregationMode.DIFF
+ },
+ {
+ label: 'Max',
+ value: tr.ui.analysis.MemoryColumn.AggregationMode.MAX
+ }
+ ]));
+ },
+
+ /**
+ * Sets the container memory dumps and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronologically sorted list of
+ * ContainerMemoryDump objects. All of the dumps must be associated with
+ * the same container (i.e. containerMemoryDumps must be either a list of
+ * ProcessMemoryDump(s) belonging to the same process, or a list of
+ * GlobalMemoryDump(s)). Example:
+ *
+ * [
+ * tr.model.ProcessMemoryDump {}, // PMD at timestamp 1.
+ * tr.model.ProcessMemoryDump {}, // PMD at timestamp 2.
+ * tr.model.ProcessMemoryDump {} // PMD at timestamp 3.
+ * ]
+ */
+ set containerMemoryDumps(containerMemoryDumps) {
+ this.containerMemoryDumps_ = containerMemoryDumps;
+ this.scheduleRebuild_();
+ },
+
+ get containerMemoryDumps() {
+ return this.containerMemoryDumps_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ onRebuild_() {
+ this.updateLabel_();
+ this.updateAggregationModeSelector_();
+ this.changeChildPane_();
+ },
+
+ updateLabel_() {
+ Polymer.dom(this.$.label).textContent = '';
+
+ if (this.containerMemoryDumps_ === undefined ||
+ this.containerMemoryDumps_.length <= 0) {
+ Polymer.dom(this.$.label).textContent = 'No memory dumps selected';
+ return;
+ }
+
+ const containerDumpCount = this.containerMemoryDumps_.length;
+ const isMultiSelection = containerDumpCount > 1;
+
+ Polymer.dom(this.$.label).appendChild(document.createTextNode(
+ 'Selected ' + containerDumpCount + ' memory dump' +
+ (isMultiSelection ? 's' : '') +
+ ' in ' + this.containerMemoryDumps_[0].containerName + ' at '));
+ // TODO(petrcermak): Use <tr-v-ui-scalar-span> once it can be displayed
+ // inline. See https://github.com/catapult-project/catapult/issues/1371.
+ Polymer.dom(this.$.label).appendChild(document.createTextNode(
+ tr.b.Unit.byName.timeStampInMs.format(
+ this.containerMemoryDumps_[0].start)));
+ if (isMultiSelection) {
+ const ELLIPSIS = String.fromCharCode(8230);
+ Polymer.dom(this.$.label).appendChild(
+ document.createTextNode(ELLIPSIS));
+ Polymer.dom(this.$.label).appendChild(document.createTextNode(
+ tr.b.Unit.byName.timeStampInMs.format(
+ this.containerMemoryDumps_[containerDumpCount - 1].start)));
+ }
+ },
+
+ updateAggregationModeSelector_() {
+ let displayStyle;
+ if (this.containerMemoryDumps_ === undefined ||
+ this.containerMemoryDumps_.length <= 1) {
+ displayStyle = 'none';
+ } else {
+ displayStyle = 'initial';
+ }
+ this.$.aggregation_mode_container.style.display = displayStyle;
+ },
+
+ changeChildPane_() {
+ this.childPaneBuilder = function() {
+ if (this.containerMemoryDumps_ === undefined ||
+ this.containerMemoryDumps_.length <= 0) {
+ return undefined;
+ }
+
+ const overviewPane = document.createElement(
+ 'tr-ui-a-memory-dump-overview-pane');
+ overviewPane.processMemoryDumps = this.containerMemoryDumps_.map(
+ function(containerDump) {
+ return containerDump.processMemoryDumps;
+ });
+ overviewPane.aggregationMode = this.aggregationMode;
+ return overviewPane;
+ }.bind(this);
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html
new file mode 100644
index 00000000000..3d5d20a7c47
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_header_pane_test.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/memory_dump_header_pane.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+
+ function createAndCheckMemoryDumpHeaderPane(test, containerMemoryDumps,
+ expectedLabelText, expectedChildPaneRequested, expectedSelectorVisible) {
+ const viewEl =
+ tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-header-pane');
+ viewEl.containerMemoryDumps = containerMemoryDumps;
+ viewEl.rebuild();
+ test.addHTMLOutput(viewEl);
+ checkMemoryDumpHeaderPane(viewEl, containerMemoryDumps, expectedLabelText,
+ expectedChildPaneRequested, expectedSelectorVisible);
+ }
+
+ function checkMemoryDumpHeaderPane(viewEl, containerMemoryDumps,
+ expectedLabelText, expectedChildPaneRequested, expectedSelectorVisible) {
+ // The default aggregation mode is DIFF.
+ assert.strictEqual(viewEl.aggregationMode, AggregationMode.DIFF);
+
+ // Check the text in the label.
+ assert.strictEqual(
+ Polymer.dom(viewEl.$.label).textContent, expectedLabelText);
+
+ // Check the visibility of aggregation mode selector.
+ const aggregationModeContainerVisible =
+ isElementDisplayed(viewEl.$.aggregation_mode_container);
+ const childPanes = viewEl.requestedChildPanes;
+
+ // Check the requested child panes.
+ if (containerMemoryDumps === undefined ||
+ containerMemoryDumps.length === 0) {
+ assert.isTrue(!expectedSelectorVisible); // Test sanity check.
+ assert.isFalse(aggregationModeContainerVisible);
+ assert.lengthOf(childPanes, 1);
+ assert.isUndefined(childPanes[0]);
+ return;
+ }
+
+ const expectedProcessMemoryDumps = containerMemoryDumps.map(
+ function(containerMemoryDump) {
+ return containerMemoryDump.processMemoryDumps;
+ });
+ function checkLastChildPane(expectedChildPaneCount) {
+ assert.lengthOf(childPanes, expectedChildPaneCount);
+ const lastChildPane = childPanes[expectedChildPaneCount - 1];
+ assert.strictEqual(
+ lastChildPane.tagName, 'TR-UI-A-MEMORY-DUMP-OVERVIEW-PANE');
+ assert.deepEqual(lastChildPane.processMemoryDumps,
+ expectedProcessMemoryDumps);
+ assert.strictEqual(lastChildPane.aggregationMode, viewEl.aggregationMode);
+ }
+
+ checkLastChildPane(1);
+
+ // Check the behavior of aggregation mode selector (if visible).
+ if (!expectedSelectorVisible) {
+ assert.isFalse(aggregationModeContainerVisible);
+ return;
+ }
+
+ assert.isTrue(aggregationModeContainerVisible);
+ const selector = tr.ui.b.findDeepElementMatching(viewEl, 'select');
+
+ selector.selectedValue = AggregationMode.MAX;
+ viewEl.rebuild();
+ assert.strictEqual(viewEl.aggregationMode, AggregationMode.MAX);
+ checkLastChildPane(2);
+
+ selector.selectedValue = AggregationMode.DIFF;
+ viewEl.rebuild();
+ assert.strictEqual(viewEl.aggregationMode, AggregationMode.DIFF);
+ checkLastChildPane(3);
+ }
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-header-pane', 'containerMemoryDumps',
+ function(viewEl) {
+ checkMemoryDumpHeaderPane(viewEl, [], 'No memory dumps selected',
+ false /* no child pane requested */,
+ false /* aggregation mode selector hidden */);
+ });
+ });
+
+ test('instantiate_singleGlobalMemoryDump', function() {
+ createAndCheckMemoryDumpHeaderPane(this,
+ [tr.ui.analysis.createSingleTestGlobalMemoryDump()],
+ 'Selected 1 memory dump in global space at 68.000 ms',
+ true /* child pane requested */,
+ false /* aggregation mode selector hidden */);
+ });
+
+ test('instantiate_multipleGlobalMemoryDumps', function() {
+ createAndCheckMemoryDumpHeaderPane(this,
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps(),
+ 'Selected 3 memory dumps in global space at 42.000 ms\u2026100.000 ms',
+ true /* child pane requested */,
+ true /* aggregation selector visible */);
+ });
+
+ test('instantiate_singleProcessMemoryDump', function() {
+ createAndCheckMemoryDumpHeaderPane(this,
+ [tr.ui.analysis.createSingleTestProcessMemoryDump()],
+ 'Selected 1 memory dump in Process 2 at 69.000 ms',
+ true /* child pane requested */,
+ false /* aggregation mode selector hidden */);
+ });
+
+ test('instantiate_multipleProcessMemoryDumps', function() {
+ createAndCheckMemoryDumpHeaderPane(this,
+ tr.ui.analysis.createMultipleTestProcessMemoryDumps(),
+ 'Selected 3 memory dumps in Process 2 at 42.000 ms\u2026102.000 ms',
+ true /* child pane requested */,
+ true /* aggregation selector visible */);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html
new file mode 100644
index 00000000000..9d17e39ce85
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html
@@ -0,0 +1,354 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/tab_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+
+<dom-module id='tr-ui-a-memory-dump-heap-details-breakdown-view'>
+ <template>
+ <tr-ui-b-tab-view id="tabs"></tr-ui-b-tab-view>
+ </template>
+</dom-module>
+
+<dom-module id='tr-ui-a-memory-dump-heap-details-breakdown-view-tab'>
+ <template>
+ <tr-v-ui-scalar-context-controller></tr-v-ui-scalar-context-controller>
+ <tr-ui-b-info-bar id="info" hidden></tr-ui-b-info-bar>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const RESONABLE_NUMBER_OF_ROWS = 200;
+
+ const TabUiState = {
+ NO_LONG_TAIL: 0,
+ HIDING_LONG_TAIL: 1,
+ SHOWING_LONG_TAIL: 2,
+ };
+
+ /** @constructor */
+ function EmptyFillerColumn() {}
+
+ EmptyFillerColumn.prototype = {
+ title: '',
+
+ value() {
+ return '';
+ },
+ };
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-heap-details-breakdown-view',
+ behaviors: [tr.ui.analysis.RebuildableBehavior],
+
+ created() {
+ this.displayedNode_ = undefined;
+ this.dimensionToTab_ = new Map();
+ },
+
+ ready() {
+ this.scheduleRebuild_();
+ this.root.addEventListener('keydown', this.onKeyDown_.bind(this), true);
+ },
+
+ get displayedNode() {
+ return this.displayedNode_;
+ },
+
+ set displayedNode(node) {
+ this.displayedNode_ = node;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ for (const tab of this.$.tabs.tabs) {
+ tab.aggregationMode = aggregationMode;
+ }
+ },
+
+ onRebuild_() {
+ const previouslySelectedTab = this.$.tabs.selectedSubView;
+ let previouslySelectedTabFocused = false;
+ let previouslySelectedDimension = undefined;
+ if (previouslySelectedTab) {
+ previouslySelectedTabFocused = previouslySelectedTab.isFocused;
+ previouslySelectedDimension = previouslySelectedTab.dimension;
+ }
+
+ for (const tab of this.$.tabs.tabs) {
+ tab.nodes = undefined;
+ }
+ this.$.tabs.clearSubViews();
+
+ if (this.displayedNode_ === undefined) {
+ this.$.tabs.label = 'No heap node provided.';
+ return;
+ }
+
+ for (const [dimension, children] of this.displayedNode_.childNodes) {
+ if (!this.dimensionToTab_.has(dimension)) {
+ this.dimensionToTab_.set(dimension, document.createElement(
+ 'tr-ui-a-memory-dump-heap-details-breakdown-view-tab'));
+ }
+ const tab = this.dimensionToTab_.get(dimension);
+ tab.aggregationMode = this.aggregationMode_;
+ tab.dimension = dimension;
+ tab.nodes = children;
+ this.$.tabs.addSubView(tab);
+ tab.rebuild();
+ if (dimension === previouslySelectedDimension) {
+ this.$.tabs.selectedSubView = tab;
+ if (previouslySelectedTabFocused) {
+ tab.focus();
+ }
+ }
+ }
+
+ if (this.$.tabs.tabs.length > 0) {
+ this.$.tabs.label = 'Break selected node further by:';
+ } else {
+ this.$.tabs.label = 'Selected node cannot be broken down any further.';
+ }
+ },
+
+ onKeyDown_(keyEvent) {
+ if (!this.displayedNode_) return;
+
+ let keyHandled = false;
+ switch (keyEvent.keyCode) {
+ case 8: {
+ // Backspace.
+ if (!this.displayedNode_.parentNode) break;
+
+ // Enter the parent node upon pressing backspace.
+ const viewEvent = new tr.b.Event('enter-node');
+ viewEvent.node = this.displayedNode_.parentNode;
+ this.dispatchEvent(viewEvent);
+ keyHandled = true;
+ break;
+ }
+
+ case 37: // Left arrow.
+ case 39: // Right arrow.
+ {
+ const wasFocused = this.$.tabs.selectedSubView.isFocused;
+ keyHandled = keyEvent.keyCode === 37 ?
+ this.$.tabs.selectPreviousTabIfPossible() :
+ this.$.tabs.selectNextTabIfPossible();
+ if (wasFocused && keyHandled) {
+ this.$.tabs.selectedSubView.focus(); // Restore focus to new tab.
+ }
+ }
+ }
+
+ if (!keyHandled) return;
+ keyEvent.stopPropagation();
+ keyEvent.preventDefault();
+ }
+ });
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-heap-details-breakdown-view-tab',
+ behaviors: [tr.ui.analysis.RebuildableBehavior],
+
+ created() {
+ this.dimension_ = undefined;
+ this.nodes_ = undefined;
+ this.aggregationMode_ = undefined;
+ this.displayLongTail_ = false;
+ },
+
+ ready() {
+ this.$.table.addEventListener('step-into', function(tableEvent) {
+ const viewEvent = new tr.b.Event('enter-node');
+ viewEvent.node = tableEvent.tableRow;
+ this.dispatchEvent(viewEvent);
+ }.bind(this));
+ },
+
+ get displayLongTail() {
+ return this.displayLongTail_;
+ },
+
+ set displayLongTail(newValue) {
+ if (this.displayLongTail === newValue) return;
+ this.displayLongTail_ = newValue;
+ this.scheduleRebuild_();
+ },
+
+ get dimension() {
+ return this.dimension_;
+ },
+
+ set dimension(dimension) {
+ this.dimension_ = dimension;
+ this.scheduleRebuild_();
+ },
+
+ get nodes() {
+ return this.nodes_;
+ },
+
+ set nodes(nodes) {
+ this.nodes_ = nodes;
+ this.scheduleRebuild_();
+ },
+
+ get nodes() {
+ return this.nodes_ || [];
+ },
+
+ get dimensionLabel_() {
+ if (this.dimension_ === undefined) return '(undefined)';
+ return this.dimension_.label;
+ },
+
+ get tabLabel() {
+ let nodeCount = 0;
+ if (this.nodes_) {
+ nodeCount = this.nodes_.length;
+ }
+ return this.dimensionLabel_ + ' (' + nodeCount + ')';
+ },
+
+ get tabIcon() {
+ if (this.dimension_ === undefined ||
+ this.dimension_ === tr.ui.analysis.HeapDetailsRowDimension.ROOT) {
+ return undefined;
+ }
+ return {
+ text: this.dimension_.symbol,
+ style: 'color: ' + tr.b.ColorScheme.getColorForReservedNameAsString(
+ this.dimension_.color) + ';'
+ };
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ focus() {
+ this.$.table.focus();
+ },
+
+ blur() {
+ this.$.table.blur();
+ },
+
+ get isFocused() {
+ return this.$.table.isFocused;
+ },
+
+ onRebuild_() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.table.emptyValue = 'Cannot break down by ' +
+ this.dimensionLabel_.toLowerCase() + ' any further.';
+ const [state, rows] = this.getRows_();
+ const total = this.nodes.length;
+ const displayed = rows.length;
+ const hidden = total - displayed;
+ this.updateInfoBar_(state, [total, displayed, hidden]);
+ this.$.table.tableRows = rows;
+ this.$.table.tableColumns = this.createColumns_(rows);
+ if (this.$.table.sortColumnIndex === undefined) {
+ this.$.table.sortColumnIndex = 0;
+ this.$.table.sortDescending = false;
+ }
+ this.$.table.rebuild();
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new tr.ui.analysis.HeapDetailsTitleColumn(
+ this.dimensionLabel_);
+ titleColumn.width = '400px';
+
+ const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'cells',
+ aggregationMode: this.aggregationMode_,
+ rules: tr.ui.analysis.HEAP_DETAILS_COLUMN_RULES,
+ shouldSetContextGroup: true
+ });
+ if (numericColumns.length === 0) {
+ numericColumns.push(new EmptyFillerColumn());
+ }
+ tr.ui.analysis.MemoryColumn.spaceEqually(numericColumns);
+
+ const columns = [titleColumn].concat(numericColumns);
+ return columns;
+ },
+
+ getRows_() {
+ let rows = this.nodes;
+ if (rows.length <= RESONABLE_NUMBER_OF_ROWS) {
+ return [TabUiState.NO_LONG_TAIL, rows];
+ } else if (this.displayLongTail) {
+ return [TabUiState.SHOWING_LONG_TAIL, rows];
+ }
+ const absSize = row => Math.max(row.cells.Size.fields[0].value);
+ rows.sort((a, b) => absSize(b) - absSize(a));
+ rows = rows.slice(0, RESONABLE_NUMBER_OF_ROWS);
+ return [TabUiState.HIDING_LONG_TAIL, rows];
+ },
+
+ updateInfoBar_(state, rowStats) {
+ if (state === TabUiState.SHOWING_LONG_TAIL) {
+ this.longTailVisibleInfoBar_(rowStats);
+ } else if (state === TabUiState.HIDING_LONG_TAIL) {
+ this.longTailHiddenInfoBar_(rowStats);
+ } else {
+ this.hideInfoBar_();
+ }
+ },
+
+ longTailVisibleInfoBar_(rowStats) {
+ const [total, visible, hidden] = rowStats;
+ const couldHide = total - RESONABLE_NUMBER_OF_ROWS;
+ this.$.info.message = 'Showing ' + total + ' rows. This may be slow.';
+ this.$.info.removeAllButtons();
+ const buttonText = 'Hide ' + couldHide + ' rows.';
+ this.$.info.addButton(buttonText, () => this.displayLongTail = false);
+ this.$.info.visible = true;
+ },
+
+ longTailHiddenInfoBar_(rowStats) {
+ const [total, visible, hidden] = rowStats;
+ this.$.info.message = 'Hiding the smallest ' + hidden + ' rows.';
+ this.$.info.removeAllButtons();
+ this.$.info.addButton('Show all.', () => this.displayLongTail = true);
+ this.$.info.visible = true;
+ },
+
+ hideInfoBar_() {
+ this.$.info.visible = false;
+ },
+
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html
new file mode 100644
index 00000000000..a43fdaa8189
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html
@@ -0,0 +1,451 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_breakdown_view.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_path_view.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+
+<dom-module id='tr-ui-a-memory-dump-heap-details-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #header {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ background-color: #eee;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ }
+
+ #label {
+ flex: 1 1 auto;
+ padding: 8px;
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ #view_mode_container {
+ display: none;
+ flex: 0 0 auto;
+ padding: 5px;
+ font-size: 15px;
+ }
+
+ #contents {
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+
+ #info_text {
+ padding: 8px;
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ #split_view {
+ display: none; /* Hide until memory allocator dumps are set. */
+ flex: 1 0 auto;
+ align-self: stretch;
+ flex-direction: row;
+ }
+
+ #path_view {
+ width: 50%;
+ }
+
+ #breakdown_view {
+ flex: 1 1 auto;
+ width: 0;
+ }
+
+ #path_view, #breakdown_view {
+ overflow-x: auto; /* Show scrollbar if necessary. */
+ }
+ </style>
+ <div id="header">
+ <div id="label">Heap details</div>
+ <div id="view_mode_container">
+ <span>View mode:</span>
+ <!-- View mode selector (added in Polymer.ready()) -->
+ </div>
+ </div>
+ <div id="contents">
+ <tr-ui-b-info-bar id="info_bar" hidden>
+ </tr-ui-b-info-bar>
+
+ <div id="info_text">No heap dump selected</div>
+
+ <div id="split_view">
+ <tr-ui-a-memory-dump-heap-details-path-view id="path_view">
+ </tr-ui-a-memory-dump-heap-details-path-view>
+ <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle>
+ <tr-ui-a-memory-dump-heap-details-breakdown-view id="breakdown_view">
+ </tr-ui-a-memory-dump-heap-details-breakdown-view>
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter;
+ const MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder;
+ const TotalState = tr.b.MultiDimensionalViewNode.TotalState;
+
+ /** @{constructor} */
+ function HeapDumpTreeNode(
+ stackFrameNodes, dimension, title, heavyView, parentNode) {
+ this.dimension = dimension;
+ this.title = title;
+ this.parentNode = parentNode;
+
+ this.heavyView_ = heavyView;
+ this.stackFrameNodes_ = stackFrameNodes;
+ this.lazyCells_ = undefined;
+ this.lazyChildNodes_ = undefined;
+ }
+
+ HeapDumpTreeNode.prototype = {
+ get minDisplayedTotalState_() {
+ if (this.heavyView_) {
+ // Show lower-bound and exact values in heavy views.
+ return TotalState.LOWER_BOUND;
+ }
+ // Show only exact values in tree view.
+ return TotalState.EXACT;
+ },
+
+ get childNodes() {
+ if (!this.lazyChildNodes_) {
+ this.lazyChildNodes_ = new Map();
+ this.addDimensionChildNodes_(
+ tr.ui.analysis.HeapDetailsRowDimension.STACK_FRAME, 0);
+ this.addDimensionChildNodes_(
+ tr.ui.analysis.HeapDetailsRowDimension.OBJECT_TYPE, 1);
+ this.releaseStackFrameNodesIfPossible_();
+ }
+ return this.lazyChildNodes_;
+ },
+
+ get cells() {
+ if (!this.lazyCells_) {
+ this.addCells_();
+ this.releaseStackFrameNodesIfPossible_();
+ }
+ return this.lazyCells_;
+ },
+
+ releaseStackFrameNodesIfPossible_() {
+ if (this.lazyCells_ && this.lazyChildNodes_) {
+ // Don't unnecessarily hold a reference to the stack frame nodes when
+ // we don't need them anymore.
+ this.stackFrameNodes_ = undefined;
+ }
+ },
+
+ addDimensionChildNodes_(dimension, dimensionIndex) {
+ // Child title -> Timestamp (list index) -> Child
+ // MultiDimensionalViewNode.
+ const dimensionChildTitleToStackFrameNodes = tr.b.invertArrayOfDicts(
+ this.stackFrameNodes_,
+ node => this.convertStackFrameNodeDimensionToChildDict_(
+ node, dimensionIndex));
+
+ // Child title (list index) -> Child HeapDumpTreeNode.
+ const dimensionChildNodes = [];
+ for (const [childTitle, childStackFrameNodes] of
+ Object.entries(dimensionChildTitleToStackFrameNodes)) {
+ dimensionChildNodes.push(new HeapDumpTreeNode(childStackFrameNodes,
+ dimension, childTitle, this.heavyView_, this));
+ }
+ this.lazyChildNodes_.set(dimension, dimensionChildNodes);
+ },
+
+ convertStackFrameNodeDimensionToChildDict_(
+ stackFrameNode, dimensionIndex) {
+ const childDict = {};
+ let displayedChildrenTotalSize = 0;
+ let displayedChildrenTotalCount = 0;
+ let hasDisplayedChildren = false;
+ let allDisplayedChildrenHaveDisplayedCounts = true;
+ for (const child of stackFrameNode.children[dimensionIndex].values()) {
+ if (child.values[0].totalState < this.minDisplayedTotalState_) {
+ continue;
+ }
+ if (child.values[1].totalState < this.minDisplayedTotalState_) {
+ allDisplayedChildrenHaveDisplayedCounts = false;
+ }
+ childDict[child.title[dimensionIndex]] = child;
+ displayedChildrenTotalSize += child.values[0].total;
+ displayedChildrenTotalCount += child.values[1].total;
+ hasDisplayedChildren = true;
+ }
+
+ const nodeTotalSize = stackFrameNode.values[0].total;
+ const nodeTotalCount = stackFrameNode.values[1].total;
+
+ // Add '<other>' node if necessary in tree-view.
+ const hasUnclassifiedSizeOrCount =
+ displayedChildrenTotalSize < nodeTotalSize ||
+ displayedChildrenTotalCount < nodeTotalCount;
+ if (!this.heavyView_ && hasUnclassifiedSizeOrCount &&
+ hasDisplayedChildren) {
+ const otherTitle = stackFrameNode.title.slice();
+ otherTitle[dimensionIndex] = '<other>';
+ const otherNode = new tr.b.MultiDimensionalViewNode(otherTitle, 2);
+ childDict[otherTitle[dimensionIndex]] = otherNode;
+
+ // '<other>' node size.
+ otherNode.values[0].total = nodeTotalSize - displayedChildrenTotalSize;
+ otherNode.values[0].totalState = this.minDisplayedTotalState_;
+
+ // '<other>' node allocation count.
+ otherNode.values[1].total =
+ nodeTotalCount - displayedChildrenTotalCount;
+ // Don't show allocation count of the '<other>' node if there is a
+ // displayed child node that did NOT display allocation count.
+ otherNode.values[1].totalState =
+ allDisplayedChildrenHaveDisplayedCounts ?
+ this.minDisplayedTotalState_ : TotalState.NOT_PROVIDED;
+ }
+
+ return childDict;
+ },
+
+ addCells_() {
+ // Transform a chronological list of heap stack frame tree nodes into a
+ // dictionary of cells (where each cell contains a chronological list
+ // of the values of its numeric).
+ this.lazyCells_ = tr.ui.analysis.createCells(this.stackFrameNodes_,
+ function(stackFrameNode) {
+ const size = stackFrameNode.values[0].total;
+ const numerics = {
+ 'Size': new Scalar(sizeInBytes_smallerIsBetter, size)
+ };
+ const countValue = stackFrameNode.values[1];
+ if (countValue.totalState >= this.minDisplayedTotalState_) {
+ const count = countValue.total;
+ numerics.Count = new Scalar(
+ count_smallerIsBetter, count);
+ }
+ return numerics;
+ }, this);
+ }
+ };
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-heap-details-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.heapDumps_ = undefined;
+ this.viewMode_ = undefined;
+ this.aggregationMode_ = undefined;
+ this.cachedBuilders_ = new Map();
+ },
+
+ ready() {
+ this.$.info_bar.message = 'Note: Values displayed in the heavy view ' +
+ 'are lower bounds (except for the root).';
+
+ Polymer.dom(this.$.view_mode_container).appendChild(
+ tr.ui.b.createSelector(
+ this, 'viewMode', 'memoryDumpHeapDetailsPane.viewMode',
+ MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW,
+ [
+ {
+ label: 'Top-down (Tree)',
+ value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW
+ },
+ {
+ label: 'Top-down (Heavy)',
+ value:
+ MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW
+ },
+ {
+ label: 'Bottom-up (Heavy)',
+ value:
+ MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW
+ }
+ ]));
+
+ this.$.drag_handle.target = this.$.path_view;
+ this.$.drag_handle.horizontal = false;
+
+ // If the user selects a node in the path view, show its children in the
+ // breakdown view.
+ this.$.path_view.addEventListener('selected-node-changed', (function(e) {
+ this.$.breakdown_view.displayedNode = this.$.path_view.selectedNode;
+ }).bind(this));
+
+ // If the user double-clicks on a node in the breakdown view, select the
+ // node in the path view.
+ this.$.breakdown_view.addEventListener('enter-node', (function(e) {
+ this.$.path_view.selectedNode = e.node;
+ }).bind(this));
+ },
+
+ /**
+ * Sets the heap dumps and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronological list of heap dumps. All
+ * dumps are assumed to belong to the same process and belong to the same
+ * allocator. Example:
+ *
+ * [
+ * tr.model.HeapDump {}, // Heap dump at timestamp 1.
+ * undefined, // Heap dump not provided at timestamp 2.
+ * tr.model.HeapDump {}, // Heap dump at timestamp 3.
+ * ]
+ */
+ set heapDumps(heapDumps) {
+ this.heapDumps_ = heapDumps;
+ this.scheduleRebuild_();
+ },
+
+ get heapDumps() {
+ return this.heapDumps_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.$.path_view.aggregationMode = aggregationMode;
+ this.$.breakdown_view.aggregationMode = aggregationMode;
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ set viewMode(viewMode) {
+ this.viewMode_ = viewMode;
+ this.scheduleRebuild_();
+ },
+
+ get viewMode() {
+ return this.viewMode_;
+ },
+
+ get heavyView() {
+ switch (this.viewMode) {
+ case MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW:
+ case MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW:
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ onRebuild_() {
+ if (this.heapDumps_ === undefined ||
+ this.heapDumps_.length === 0) {
+ // Show the info text (hide the table and the view mode selector).
+ this.$.info_text.style.display = 'block';
+ this.$.split_view.style.display = 'none';
+ this.$.view_mode_container.style.display = 'none';
+ this.$.info_bar.hidden = true;
+ this.$.path_view.selectedNode = undefined;
+ return;
+ }
+
+ // Show the table and the view mode selector (hide the info text).
+ this.$.info_text.style.display = 'none';
+ this.$.split_view.style.display = 'flex';
+ this.$.view_mode_container.style.display = 'block';
+
+ // Show the info bar if in heavy view mode.
+ this.$.info_bar.hidden = !this.heavyView;
+
+ this.$.path_view.selectedNode = this.createHeapTree_();
+ this.$.path_view.rebuild();
+ this.$.breakdown_view.rebuild();
+ },
+
+ createHeapTree_() {
+ const definedHeapDump = this.heapDumps_.find(x => x);
+ if (definedHeapDump === undefined) return undefined;
+
+ // The title of the root node is the name of the allocator.
+ const rootRowTitle = definedHeapDump.allocatorName;
+
+ const stackFrameTrees = this.createStackFrameTrees_(this.heapDumps_);
+
+ return new HeapDumpTreeNode(stackFrameTrees,
+ tr.ui.analysis.HeapDetailsRowDimension.ROOT, rootRowTitle,
+ this.heavyView);
+ },
+
+ createStackFrameTrees_(heapDumps) {
+ const builders = heapDumps.map(heapDump => this.createBuilder_(heapDump));
+ const views = builders.map(builder => {
+ if (builder === undefined) return undefined;
+ return builder.buildView(this.viewMode);
+ });
+ return views;
+ },
+
+ createBuilder_(heapDump) {
+ if (heapDump === undefined) return undefined;
+
+ if (this.cachedBuilders_.has(heapDump)) {
+ return this.cachedBuilders_.get(heapDump);
+ }
+
+ const dimensions = 2; // stack frames, object type
+ const valueCount = 2; // size, count
+ const builder = new MultiDimensionalViewBuilder(dimensions, valueCount);
+
+ // Build the heap tree.
+ for (const entry of heapDump.entries) {
+ const leafStackFrame = entry.leafStackFrame;
+ const stackTracePath = leafStackFrame === undefined ?
+ [] : leafStackFrame.getUserFriendlyStackTrace().reverse();
+
+ const objectTypeName = entry.objectTypeName;
+ const objectTypeNamePath = objectTypeName === undefined ?
+ [] : [objectTypeName];
+
+ const valueKind = entry.valuesAreTotals ?
+ MultiDimensionalViewBuilder.ValueKind.TOTAL :
+ MultiDimensionalViewBuilder.ValueKind.SELF;
+
+ builder.addPath([stackTracePath, objectTypeNamePath],
+ [entry.size, entry.count],
+ valueKind);
+ }
+
+ builder.complete = heapDump.isComplete;
+ this.cachedBuilders_.set(heapDump, builder);
+ return builder;
+ },
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html
new file mode 100644
index 00000000000..947cc532e7a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_pane_test.html
@@ -0,0 +1,4045 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel='import' href='/tracing/base/multi_dimensional_view.html'>
+<link rel='import' href='/tracing/base/unit.html'>
+<link rel='import' href='/tracing/base/utils.html'>
+<link rel='import' href='/tracing/core/test_utils.html'>
+<link rel='import' href='/tracing/model/heap_dump.html'>
+<link rel='import' href='/tracing/model/memory_dump_test_utils.html'>
+<link rel='import'
+ href='/tracing/ui/analysis/memory_dump_heap_details_pane.html'>
+<link rel='import'
+ href='/tracing/ui/analysis/memory_dump_sub_view_test_utils.html'>
+<link rel='import' href='/tracing/ui/analysis/memory_dump_sub_view_util.html'>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ViewType = tr.b.MultiDimensionalViewBuilder.ViewType;
+ const TOP_DOWN_TREE_VIEW = ViewType.TOP_DOWN_TREE_VIEW;
+ const TOP_DOWN_HEAVY_VIEW = ViewType.TOP_DOWN_HEAVY_VIEW;
+ const BOTTOM_UP_HEAVY_VIEW = ViewType.BOTTOM_UP_HEAVY_VIEW;
+ const HeapDump = tr.model.HeapDump;
+ const HeapDetailsRowDimension = tr.ui.analysis.HeapDetailsRowDimension;
+ const ROOT = HeapDetailsRowDimension.ROOT;
+ const STACK_FRAME = HeapDetailsRowDimension.STACK_FRAME;
+ const OBJECT_TYPE = HeapDetailsRowDimension.OBJECT_TYPE;
+ const TitleColumn = tr.ui.analysis.TitleColumn;
+ const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn;
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const checkColumns = tr.ui.analysis.checkColumns;
+ const checkNumericFields = tr.ui.analysis.checkNumericFields;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+ const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter;
+
+ function createHeapDumps(withCount) {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(1);
+
+ function addHeapEntry(heapDump, stackFrames, objectTypeName, size, count) {
+ const leafStackFrame = stackFrames === undefined ? undefined :
+ tr.c.TestUtils.newStackTrace(model, stackFrames);
+ heapDump.addEntry(leafStackFrame, objectTypeName, size,
+ withCount ? count : undefined);
+ }
+
+ // First timestamp.
+ const gmd1 = addGlobalMemoryDump(model, {ts: -10});
+ const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11});
+ const hd1 = new HeapDump(pmd1, 'partition_alloc');
+
+ addHeapEntry(hd1, undefined /* sum over all traces */,
+ undefined /* sum over all types */, 4194304 /* 4 MiB */, 1000);
+ addHeapEntry(hd1, undefined /* sum over all traces */, 'v8::Context',
+ 1048576 /* 1 MiB */, 200);
+ addHeapEntry(hd1, undefined /* sum over all traces */, 'blink::Node',
+ 331776 /* 324 KiB */, 10);
+ addHeapEntry(hd1, ['MessageLoop::RunTask'],
+ undefined /* sum over all types */, 4194304 /* 4 MiB */, 1000);
+ addHeapEntry(hd1, ['MessageLoop::RunTask'], 'v8::Context',
+ 1048576 /* 1 MiB */, 200);
+
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'],
+ undefined /* sum over all types */, 1406976 /* 1.3 MiB */, 299);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'],
+ 'blink::Node', 331776 /* 324 KiB */, 10);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall'], 'v8::Context',
+ 1024000 /* 1000 KiB */, 176);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', '<self>'],
+ undefined /* sum over all types */, 102400 /* 100 KiB */, 30);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ 'v8::Context', 716800 /* 700 KiB */, 100);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ undefined /* sum over all types */, 1048576 /* 1 MiB */, 101);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'],
+ undefined /* sum over all types */,
+ 153600 /* 150 KiB, lower than the actual sum (should be ignored) */,
+ 25 /* the allocation count should, however, NOT be ignored */);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'],
+ 'v8::Context', 153600 /* 150 KiB */, 15);
+
+ // The following entry should not appear in the tree-view because there is
+ // no entry for its parent stack frame.
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'MissingParent', 'FunctionCall'],
+ undefined /* sum over all types */, 10 /* 10 B */, 2);
+
+ // The following entry should not appear in the tree-view because there is
+ // no sum over all types (for the given stack trace). However, it will lead
+ // to a visible increase of the (incorrectly provided) sum over all types
+ // of MessageLoop::RunTask -> FunctionCall -> FunctionCall by 50 KiB.
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall',
+ 'FunctionCall'],
+ 'MissingSumOverAllTypes', 51200 /* 50 KiB */, 9);
+
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute'],
+ undefined /* sum over all types */, 2404352 /* 2.3 MiB */, 399);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ undefined /* sum over all types */, 2404352 /* 2.3 MiB */, 399);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ 'v8::Context', 20480 /* 20 KiB */, 6);
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', '<self>'],
+ 'v8::Context', 15360 /* 15 KiB */, 5);
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute'],
+ undefined /* sum over all types */, 2097152 /* 2 MiB */, 99);
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute',
+ 'V8.Execute'],
+ undefined /* sum over all types */, 2097152 /* 2 MiB */, 99);
+ addHeapEntry(hd1,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', '<self>'],
+ undefined /* sum over all types */, 307200 /* 300 KiB */, 300);
+
+ // Second timestamp.
+ const gmd2 = addGlobalMemoryDump(model, {ts: 10});
+ const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 11});
+ const hd2 = new HeapDump(pmd2, 'partition_alloc');
+
+ addHeapEntry(hd2, undefined /* sum over all traces */,
+ undefined /* sum over all types */,
+ 3145728 /* 3 MiB, lower than the actual sum (should be ignored) */,
+ 900 /* the allocation count should, however, NOT be ignored */);
+ addHeapEntry(hd2, undefined /* sum over all traces */,
+ 'v8::Context', 1258291 /* 1.2 MiB */, 520);
+ addHeapEntry(hd2, undefined /* sum over all traces */,
+ 'blink::Node', 1048576 /* 1 MiB */, 5);
+ addHeapEntry(hd2, ['<self>'], undefined /* sum over all types */,
+ 131072 /* 128 KiB */, 16);
+ addHeapEntry(hd2, ['<self>'], 'v8::Context', 131072 /* 128 KiB */, 16);
+ addHeapEntry(hd2, ['MessageLoop::RunTask'],
+ undefined /* sum over all types */, 4823449 /* 4.6 MiB */, 884);
+ addHeapEntry(hd2, ['MessageLoop::RunTask'], 'v8::Context',
+ 1127219 /* 1.1 MiB */, 317);
+
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'],
+ undefined /* sum over all types */, 2170880 /* 2.1 MiB */, 600);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'], 'v8::Context',
+ 1024000 /* 1000 KiB */, 500);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall'], 'blink::Node',
+ 819200 /* 800 KiB */, 4);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ undefined /* sum over all types */, 1572864 /* 1.5 MiB */, 270);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ 'v8::Context', 614400 /* 600 KiB */, 123);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'V8.Execute'],
+ 'blink::Node', 819200 /* 800 KiB */, 4);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'],
+ undefined /* sum over all types */, 204800 /* 200 KiB */, 313);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall'],
+ 'v8::Context', 122880 /* 120 KiB */, 270);
+ addHeapEntry(hd2,
+ ['MessageLoop::RunTask', 'FunctionCall', 'FunctionCall',
+ 'FunctionCall'],
+ undefined /* sum over all types */, 204800 /* 200 KiB */, 313);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'FunctionCall', '<self>'],
+ undefined /* sum over all types */, 393216 /* 384 KiB */, 17);
+
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute'],
+ undefined /* sum over all types */, 2621440 /* 2.5 MiB */, 199);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ undefined /* sum over all types */, 2621440 /* 2.5 MiB */, 199);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ 'v8::Context', 20480 /* 20 KiB */, 4);
+ addHeapEntry(hd2, ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall'],
+ 'WTF::StringImpl', 126362 /* 123.4 KiB */, 56);
+ addHeapEntry(hd2,
+ ['MessageLoop::RunTask', 'V8.Execute', 'FunctionCall', 'V8.Execute'],
+ undefined /* sum over all types */, 2516582 /* 2.4 MiB */, 158);
+
+ return [hd1, hd2];
+ }
+
+ function createSelfHeapDumps(withCount) {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(1);
+
+ function addHeapEntry(heapDump, stackFrames, objectTypeName, size, count) {
+ const leafStackFrame = stackFrames === undefined ? undefined :
+ tr.c.TestUtils.newStackTrace(model, stackFrames);
+ heapDump.addEntry(leafStackFrame, objectTypeName, size,
+ withCount ? count : undefined, false /* valuesAreTotals */);
+ }
+
+ // First timestamp.
+ const gmd1 = addGlobalMemoryDump(model, {ts: -10});
+ const pmd1 = addProcessMemoryDump(gmd1, process, {ts: -11});
+ const hd1 = new HeapDump(pmd1, 'partition_alloc');
+ hd1.isComplete = true;
+
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'AllocSomething'],
+ 'v8::Context', 1024, 100);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'b', 'AllocSomething'],
+ 'v8::Context', 1024, 100);
+ addHeapEntry(hd1, ['MessageLoop::RunTask', 'a', 'b', 'c', 'AllocSomething'],
+ 'v8::Context', 1024, 100);
+
+ return [hd1];
+ }
+
+
+ function checkDisplayedElements(viewEl, displayExpectations) {
+ assert.strictEqual(isElementDisplayed(viewEl.$.info_text),
+ displayExpectations.infoText);
+ assert.strictEqual(isElementDisplayed(viewEl.$.info_bar),
+ displayExpectations.infoBar);
+ assert.strictEqual(isElementDisplayed(viewEl.$.split_view),
+ displayExpectations.tableAndSplitView);
+ assert.strictEqual(isElementDisplayed(viewEl.$.view_mode_container),
+ displayExpectations.tableAndSplitView);
+ }
+
+ const EXPECTED_COLUMNS_WITHOUT_COUNT = [
+ { title: 'Current path', type: TitleColumn, noAggregation: true },
+ { title: 'Size', type: NumericMemoryColumn }
+ ];
+
+ const EXPECTED_COLUMNS_WITH_COUNT = EXPECTED_COLUMNS_WITHOUT_COUNT.concat([
+ { title: 'Count', type: NumericMemoryColumn },
+ ]);
+
+ const EXPECTED_CELLS = ['Size', 'Count'];
+
+ function checkNode(node, expectedNodeStructure, expectedParentNode) {
+ assert.strictEqual(node.title, expectedNodeStructure.title);
+ assert.strictEqual(node.dimension, expectedNodeStructure.dimension);
+ assert.strictEqual(node.parentNode, expectedParentNode);
+
+ // Check that there AREN'T any cells that we are NOT expecting.
+ const cells = node.cells;
+ assert.includeMembers(EXPECTED_CELLS, Object.keys(cells));
+
+ const sizeCell = cells.Size;
+ const sizeFields = sizeCell ? sizeCell.fields : undefined;
+ checkSizeNumericFields(sizeFields, undefined, expectedNodeStructure.size);
+
+ const countCell = cells.Count;
+ const countFields = countCell ? countCell.fields : undefined;
+ checkNumericFields(countFields, undefined, expectedNodeStructure.count,
+ count_smallerIsBetter);
+
+ assert.strictEqual(node.childNodes.size, 2);
+
+ // If |expectedNodeStructure.children| is undefined, check that there are
+ // no child nodes.
+ if (!expectedNodeStructure.children) {
+ assert.lengthOf(node.childNodes.get(STACK_FRAME), 0);
+ assert.lengthOf(node.childNodes.get(OBJECT_TYPE), 0);
+ return;
+ }
+
+ // If |expectedNodeStructure.children| is just a number, check total number
+ // of child nodes.
+ if (typeof expectedNodeStructure.children === 'number') {
+ assert.strictEqual(expectedNodeStructure.children,
+ node.childNodes.get(STACK_FRAME).length +
+ node.childNodes.get(OBJECT_TYPE).length);
+ return;
+ }
+
+ // Check child nodes wrt both dimensions.
+ checkNodes(node.childNodes.get(STACK_FRAME),
+ expectedNodeStructure.children.filter(c => c.dimension === STACK_FRAME),
+ node);
+ checkNodes(node.childNodes.get(OBJECT_TYPE),
+ expectedNodeStructure.children.filter(c => c.dimension === OBJECT_TYPE),
+ node);
+ }
+
+ function checkNodes(nodes, expectedStructure, expectedParentNode) {
+ assert.lengthOf(nodes, expectedStructure.length);
+ for (let i = 0; i < expectedStructure.length; i++) {
+ checkNode(nodes[i], expectedStructure[i], expectedParentNode);
+ }
+ }
+
+ function checkSplitView(viewEl, expectedConfig, expectedStructure) {
+ checkDisplayedElements(viewEl, {
+ infoText: false,
+ tableAndSplitView: true,
+ infoBar: !!expectedConfig.expectedInfoBarDisplayed
+ });
+
+ // Both the split view and breakdown view should be displaying the same
+ // node.
+ const selectedNode = viewEl.$.path_view.selectedNode;
+ assert.strictEqual(viewEl.$.breakdown_view.displayedNode, selectedNode);
+ checkNodes([selectedNode], expectedStructure,
+ undefined /* expectedParentNode */);
+
+ // TODO: Add proper tests for tr-ui-a-memory-dump-heap-details-path-view
+ // and tr-ui-a-memory-dump-heap-details-breakdown-view.
+ const expectedColumns = expectedConfig.expectedCountColumns ?
+ EXPECTED_COLUMNS_WITH_COUNT : EXPECTED_COLUMNS_WITHOUT_COUNT;
+ checkColumns(viewEl.$.path_view.$.table.tableColumns, expectedColumns,
+ expectedConfig.expectedAggregationMode);
+ }
+
+ function changeView(viewEl, viewType) {
+ tr.ui.b.findDeepElementMatching(viewEl, 'select').selectedValue = viewType;
+ viewEl.rebuild();
+ }
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-heap-details-pane', 'heapDumps',
+ function(viewEl) {
+ // Check that the info text is shown.
+ checkDisplayedElements(viewEl, {
+ infoText: true,
+ tableAndSplitView: false,
+ infoBar: false
+ });
+ });
+ });
+
+ test('instantiate_noEntries', function() {
+ const heapDumps = createHeapDumps(false).slice(0, 1);
+ heapDumps[0].entries = [];
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Top-down tree view (default).
+ checkSplitView(viewEl,
+ { /* empty expectedConfig */ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [0],
+ defined: [true]
+ }
+ ]);
+
+ changeView(viewEl, TOP_DOWN_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ { expectedInfoBarDisplayed: true },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [0],
+ defined: [true]
+ }
+ ]);
+
+ changeView(viewEl, BOTTOM_UP_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ { expectedInfoBarDisplayed: true },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [0],
+ defined: [true]
+ }
+ ]);
+
+ changeView(viewEl, TOP_DOWN_TREE_VIEW);
+ });
+
+ test('instantiate_single', function() {
+ const heapDumps = createHeapDumps(false).slice(0, 1);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Top-down tree view (default).
+ checkSplitView(viewEl,
+ { /* empty expectedConfig */ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [331776],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600 + 51200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [51200],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [291840],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [5120],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [2383872],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [382976],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [24576],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [3145728],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [24576],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [2813952],
+ defined: [true],
+ }
+ ]
+ }
+ ]);
+
+ changeView(viewEl, BOTTOM_UP_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ { expectedInfoBarDisplayed: true },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [3811338],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1406976],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [204800],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [204800],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [153600],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [10],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1044480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1024000],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [153600],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [409600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [409600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [102400],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [3452928],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [3145728],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [2097152],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [716800],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [2097152],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [737280],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [716800],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [716800],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [10],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1044480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1024000],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [153600],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [15360],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [15360],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [737280],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [716800],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [716800],
+ defined: [true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [20480],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]);
+
+ changeView(viewEl, TOP_DOWN_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ { expectedInfoBarDisplayed: true }, [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600 + 51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [10],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976 + 10 + 2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400 + 307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576 + 2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600 + 51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000 + 20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400 + 307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576 + 2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200],
+ defined: [true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152],
+ defined: [true],
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800 + 20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [10],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000 + 20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800 + 20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776],
+ defined: [true],
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200],
+ defined: [true],
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]);
+ });
+
+ test('instantiate_multipleDiff', function() {
+ const heapDumps = createHeapDumps(true /* with allocation counts */);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ changeView(viewEl, TOP_DOWN_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ {
+ expectedAggregationMode: AggregationMode.DIFF,
+ expectedInfoBarDisplayed: true,
+ expectedCountColumns: true
+ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304, 4954521],
+ count: [1000, 900],
+ averageSize: [4194304 / 1000, 4954521 / 900],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304, 4823449],
+ count: [1000, 884],
+ averageSize: [4194304 / 1000, 4823449 / 884],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976, 2170880],
+ count: [299, 600],
+ averageSize: [1406976 / 299, 2170880 / 600],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400, 393216],
+ count: [30, 17],
+ averageSize: [102400 / 30, 393216 / 17],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576, 1572864],
+ count: [101, 270],
+ averageSize: [1048576 / 101, 1572864 / 270],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [204800, 204800],
+ count: [25, 313],
+ averageSize: [204800 / 25, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, 204800],
+ count: [9, 313],
+ averageSize: [51200 / 9, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10, undefined],
+ count: [2, undefined],
+ averageSize: [10 / 2, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [10, undefined],
+ count: [2, undefined],
+ averageSize: [10 / 2, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200, undefined],
+ count: [300, undefined],
+ averageSize: [307200 / 300, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, 2516582],
+ count: [99, 158],
+ averageSize: [2097152 / 99, 2516582 / 158],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576, 1127219],
+ count: [200, 504],
+ averageSize: [1048576 / 200, 1127219 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [3811338, 4792320],
+ count: [700, 799],
+ averageSize: [3811338 / 700, 4792320 / 799],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [409600, 393216],
+ count: [330, 17],
+ averageSize: [409600 / 330, 393216 / 17],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [3145728, 4089446],
+ count: [200, 428],
+ averageSize: [3145728 / 200, 4089446 / 428],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [204800, 204800],
+ count: [25, 313],
+ averageSize: [204800 / 25, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, 204800],
+ count: [9, 313],
+ averageSize: [51200 / 9, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1044480, 1044480],
+ count: [182, 504],
+ averageSize: [1044480 / 182, 1044480 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [409600, 524288],
+ count: [330, 33],
+ averageSize: [409600 / 330, 524288 / 33],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, 131072],
+ count: [5, 16],
+ averageSize: [15360 / 5, 131072 / 16],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [3452928, 4194304],
+ count: [500, 469],
+ averageSize: [3452928 / 500, 4194304 / 469],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200, undefined],
+ count: [300, undefined],
+ averageSize: [307200 / 300, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, 2516582],
+ count: [99, 158],
+ averageSize: [2097152 / 99, 2516582 / 158],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [737280, 634880],
+ count: [106, 127],
+ averageSize: [737280 / 106, 634880 / 127],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'MissingParent',
+ size: [10, undefined],
+ count: [2, undefined],
+ averageSize: [10 / 2, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [10, undefined],
+ count: [2, undefined],
+ averageSize: [10 / 2, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576, 1258291],
+ count: [200, 520],
+ averageSize: [1048576 / 200, 1258291 / 520],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576, 1127219],
+ count: [200, 504],
+ averageSize: [1048576 / 200, 1127219 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1044480, 1044480],
+ count: [182, 504],
+ averageSize: [1044480 / 182, 1044480 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, 131072],
+ count: [5, 16],
+ averageSize: [15360 / 5, 131072 / 16],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [737280, 634880],
+ count: [106, 127],
+ averageSize: [737280 / 106, 634880 / 127],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 1048576],
+ count: [10, 5],
+ averageSize: [331776 / 10, 1048576 / 5],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'MissingSumOverAllTypes',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [51200, undefined],
+ count: [9, undefined],
+ averageSize: [51200 / 9, undefined],
+ defined: [true, false]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]);
+
+ changeView(viewEl, TOP_DOWN_TREE_VIEW);
+ checkSplitView(viewEl,
+ {
+ expectedAggregationMode: AggregationMode.DIFF,
+ expectedCountColumns: true
+ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304, 4954521],
+ count: [1000, 900],
+ averageSize: [4194304 / 1000, 4954521 / 900],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [4194304, 4823449],
+ count: [1000, 884],
+ averageSize: [4194304 / 1000, 4823449 / 884],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1406976, 2170880],
+ count: [299, 600],
+ averageSize: [1406976 / 299, 2170880 / 600],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [102400, 393216],
+ count: [30, 17],
+ averageSize: [102400 / 30, 393216 / 17],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [1048576, 1572864],
+ count: [101, 270],
+ averageSize: [1048576 / 101, 1572864 / 270],
+ defined: [true, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [331776, 139264],
+ count: [1, 143],
+ averageSize: [331776 / 1, 139264 / 143],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [204800, 204800],
+ count: [25, 313],
+ averageSize: [204800 / 25, 204800 / 313],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [undefined, 204800],
+ count: [undefined, 313],
+ averageSize: [undefined, 204800 / 313],
+ defined: [false, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [51200, 81920],
+ count: [10, 43],
+ averageSize: [51200 / 10, 81920 / 43],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [51200, undefined],
+ count: [143, undefined],
+ averageSize: [51200 / 143, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 819200],
+ count: [10, 4],
+ averageSize: [331776 / 10, 819200 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [undefined, 819200],
+ count: [undefined, 4],
+ averageSize: [undefined, 819200 / 4],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600, 286720],
+ count: [61, 107],
+ averageSize: [153600 / 61, 286720 / 107],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [51200, 327680],
+ count: [113, 96],
+ averageSize: [51200 / 113, 327680 / 96],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [2404352, 2621440],
+ count: [399, 199],
+ averageSize: [2404352 / 399, 2621440 / 199],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [307200, undefined],
+ count: [300, undefined],
+ averageSize: [307200 / 300, undefined],
+ defined: [true, false],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [291840, undefined],
+ count: [295, undefined],
+ averageSize: [291840 / 295, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, 2516582],
+ count: [99, 158],
+ averageSize: [2097152 / 99, 2516582 / 158],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [2097152, undefined],
+ count: [99, undefined],
+ averageSize: [2097152 / 99, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [undefined, 104858],
+ count: [undefined, 41],
+ averageSize: [undefined, 104858 / 41],
+ defined: [false, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [20480, 20480],
+ count: [6, 4],
+ averageSize: [20480 / 6, 20480 / 4],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [15360, undefined],
+ count: [5, undefined],
+ averageSize: [15360 / 5, undefined],
+ defined: [true, false]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [5120, undefined],
+ count: [1, undefined],
+ averageSize: [5120 / 1, undefined],
+ defined: [true, false]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [2383872, 2474598],
+ count: [393, 139],
+ averageSize: [2383872 / 393, 2474598 / 139],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'WTF::StringImpl',
+ size: [undefined, 126362],
+ count: [undefined, 56],
+ averageSize: [undefined, 126362 / 56],
+ defined: [false, true]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [382976, 31129],
+ count: [302, 85],
+ averageSize: [382976 / 302, 31129 / 85],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576, 1127219],
+ count: [200, 504],
+ averageSize: [1048576 / 200, 1127219 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600, 286720],
+ count: [61, 107],
+ averageSize: [153600 / 61, 286720 / 107],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [24576, 103219],
+ count: [24, 4],
+ averageSize: [24576 / 24, 103219 / 4],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [3145728, 3696230],
+ count: [800, 380],
+ averageSize: [3145728 / 800, 3696230 / 380],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [undefined, 131072],
+ count: [undefined, 16],
+ averageSize: [undefined, 131072 / 16],
+ defined: [false, true],
+ children: [
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [undefined, 131072],
+ count: [undefined, 16],
+ averageSize: [undefined, 131072 / 16],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'v8::Context',
+ size: [1048576, 1258291],
+ count: [200, 520],
+ averageSize: [1048576 / 200, 1258291 / 520],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'MessageLoop::RunTask',
+ size: [1048576, 1127219],
+ count: [200, 504],
+ averageSize: [1048576 / 200, 1127219 / 504],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [1024000, 1024000],
+ count: [176, 500],
+ averageSize: [1024000 / 176, 1024000 / 500],
+ defined: [true, true],
+ children: [
+ {
+ dimension: STACK_FRAME,
+ title: 'V8.Execute',
+ size: [716800, 614400],
+ count: [100, 123],
+ averageSize: [716800 / 100, 614400 / 123],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: 'FunctionCall',
+ size: [153600, 122880],
+ count: [15, 270],
+ averageSize: [153600 / 15, 122880 / 270],
+ defined: [true, true]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [153600, 286720],
+ count: [61, 107],
+ averageSize: [153600 / 61, 286720 / 107],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<other>',
+ size: [24576, 103219],
+ count: [24, 4],
+ averageSize: [24576 / 24, 103219 / 4],
+ defined: [true, true]
+ }
+ ]
+ },
+ {
+ dimension: STACK_FRAME,
+ title: '<self>',
+ size: [undefined, 131072],
+ count: [undefined, 16],
+ averageSize: [undefined, 131072 / 16],
+ defined: [false, true]
+ }
+ ]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: 'blink::Node',
+ size: [331776, 1048576],
+ count: [10, 5],
+ averageSize: [331776 / 10, 1048576 / 5],
+ defined: [true, true]
+ },
+ {
+ dimension: OBJECT_TYPE,
+ title: '<other>',
+ size: [2813952, 2647654],
+ count: [790, 375],
+ averageSize: [2813952 / 790, 2647654 / 375],
+ defined: [true, true]
+ }
+ ]
+ }
+ ]);
+ });
+
+ test('instantiate_multipleMax', function() {
+ const heapDumps = createHeapDumps(false);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.aggregationMode = AggregationMode.MAX;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ changeView(viewEl, TOP_DOWN_HEAVY_VIEW);
+ checkSplitView(viewEl,
+ {
+ expectedAggregationMode: AggregationMode.MAX,
+ expectedInfoBarDisplayed: true
+ },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304, 4954521],
+ defined: [true, true],
+ children: 9 // No need to check the full structure again.
+ }
+ ]);
+ });
+
+ test('instantiate_multipleWithUndefined', function() {
+ const heapDumps = createHeapDumps(false);
+ heapDumps.splice(1, 0, undefined);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Top-down tree view (default).
+ checkSplitView(viewEl,
+ { expectedAggregationMode: AggregationMode.DIFF },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [4194304, undefined, 4954521],
+ defined: [true, false, true],
+ children: 5 // No need to check the full structure again.
+ }
+ ]);
+ });
+
+ test('instantiate_selfHeapSingle', function() {
+ const heapDumps = createSelfHeapDumps(true).slice(0, 1);
+
+ const viewEl = tr.ui.analysis.createTestPane(
+ 'tr-ui-a-memory-dump-heap-details-pane');
+ viewEl.heapDumps = heapDumps;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Top-down tree view (default).
+ checkSplitView(viewEl,
+ { expectedCountColumns: true },
+ [
+ {
+ dimension: ROOT,
+ title: 'partition_alloc',
+ size: [1024 * 3],
+ count: [300],
+ defined: [true],
+ children: 2 // No need to check the full structure again.
+ }
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html
new file mode 100644
index 00000000000..1cc3c8d7e5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_path_view.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_context_controller.html">
+
+<dom-module id='tr-ui-a-memory-dump-heap-details-path-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ </style>
+ <tr-v-ui-scalar-context-controller></tr-v-ui-scalar-context-controller>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const DOWNWARDS_ARROW_WITH_TIP_RIGHTWARDS = String.fromCharCode(0x21B3);
+
+ function HeapDetailsPathColumn(title) {
+ tr.ui.analysis.HeapDetailsTitleColumn.call(this, title);
+ }
+
+ HeapDetailsPathColumn.prototype = {
+ __proto__: tr.ui.analysis.HeapDetailsTitleColumn.prototype,
+
+ formatTitle(row) {
+ const title = tr.ui.analysis.HeapDetailsTitleColumn.prototype.
+ formatTitle.call(this, row);
+ if (row.dimension === tr.ui.analysis.HeapDetailsRowDimension.ROOT) {
+ return title;
+ }
+
+ const arrowEl = document.createElement('span');
+ Polymer.dom(arrowEl).textContent = DOWNWARDS_ARROW_WITH_TIP_RIGHTWARDS;
+ arrowEl.style.paddingRight = '2px';
+ arrowEl.style.fontWeight = 'bold';
+ arrowEl.style.color = tr.b.ColorScheme.getColorForReservedNameAsString(
+ 'heap_dump_child_node_arrow');
+
+ const rowEl = document.createElement('span');
+ Polymer.dom(rowEl).appendChild(arrowEl);
+ Polymer.dom(rowEl).appendChild(tr.ui.b.asHTMLOrTextNode(title));
+ return rowEl;
+ }
+ };
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-heap-details-path-view',
+ behaviors: [tr.ui.analysis.RebuildableBehavior],
+
+ created() {
+ this.selectedNode_ = undefined;
+ this.aggregationMode_ = undefined;
+ },
+
+ ready() {
+ this.$.table.addEventListener('selection-changed', function(event) {
+ this.selectedNode_ = this.$.table.selectedTableRow;
+ this.didSelectedNodeChange_();
+ }.bind(this));
+ },
+
+ didSelectedNodeChange_() {
+ this.dispatchEvent(new tr.b.Event('selected-node-changed'));
+ },
+
+ get selectedNode() {
+ return this.selectedNode_;
+ },
+
+ set selectedNode(node) {
+ this.selectedNode_ = node;
+ this.didSelectedNodeChange_();
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ onRebuild_() {
+ if (this.selectedNode_ === undefined) {
+ this.$.table.clear();
+ return;
+ }
+
+ if (this.$.table.tableRows.includes(this.selectedNode_)) {
+ this.$.table.selectedTableRow = this.selectedNode_;
+ return;
+ }
+
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.table.userCanModifySortOrder = false;
+ const rows = this.createRows_(this.selectedNode_);
+ this.$.table.tableRows = rows;
+ this.$.table.tableColumns = this.createColumns_(rows);
+ this.$.table.selectedTableRow = rows[rows.length - 1];
+ },
+
+ createRows_(node) {
+ const rows = [];
+ while (node) {
+ rows.push(node);
+ node = node.parentNode;
+ }
+ rows.reverse();
+ return rows;
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new HeapDetailsPathColumn('Current path');
+ titleColumn.width = '200px';
+
+ const numericColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'cells',
+ aggregationMode: this.aggregationMode_,
+ rules: tr.ui.analysis.HEAP_DETAILS_COLUMN_RULES,
+ shouldSetContextGroup: true
+ });
+ tr.ui.analysis.MemoryColumn.spaceEqually(numericColumns);
+
+ return [titleColumn].concat(numericColumns);
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html
new file mode 100644
index 00000000000..2cf6c6ec8b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_heap_details_util.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const LATIN_SMALL_LETTER_F_WITH_HOOK = String.fromCharCode(0x0192);
+ const CIRCLED_LATIN_CAPITAL_LETTER_T = String.fromCharCode(0x24C9);
+
+ /** @{enum} */
+ const HeapDetailsRowDimension = {
+ ROOT: {},
+ STACK_FRAME: {
+ label: 'Stack frame',
+ symbol: LATIN_SMALL_LETTER_F_WITH_HOOK,
+ color: 'heap_dump_stack_frame'
+ },
+ OBJECT_TYPE: {
+ label: 'Object type',
+ symbol: CIRCLED_LATIN_CAPITAL_LETTER_T,
+ color: 'heap_dump_object_type'
+ }
+ };
+
+ /** @{constructor} */
+ function HeapDetailsTitleColumn(title) {
+ tr.ui.analysis.TitleColumn.call(this, title);
+ }
+
+ HeapDetailsTitleColumn.prototype = {
+ __proto__: tr.ui.analysis.TitleColumn.prototype,
+
+ formatTitle(row) {
+ if (row.dimension === HeapDetailsRowDimension.ROOT) {
+ return row.title;
+ }
+
+ const symbolEl = document.createElement('span');
+ Polymer.dom(symbolEl).textContent = row.dimension.symbol;
+ symbolEl.title = row.dimension.label;
+ symbolEl.style.color = tr.b.ColorScheme.getColorForReservedNameAsString(
+ row.dimension.color);
+ symbolEl.style.paddingRight = '4px';
+ symbolEl.style.cursor = 'help';
+ symbolEl.style.fontWeight = 'bold';
+
+ const titleEl = document.createElement('span');
+ Polymer.dom(titleEl).appendChild(symbolEl);
+ Polymer.dom(titleEl).appendChild(document.createTextNode(row.title));
+
+ return titleEl;
+ }
+ };
+
+ /** @constructor */
+ function AllocationCountColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.DetailsNumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ AllocationCountColumn.prototype = {
+ __proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype,
+
+ getFormattingContext(unit) {
+ return { minimumFractionDigits: 0 };
+ }
+ };
+
+ const HEAP_DETAILS_COLUMN_RULES = [
+ {
+ condition: 'Size',
+ importance: 2,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Count',
+ importance: 1,
+ columnConstructor: AllocationCountColumn
+ },
+ {
+ importance: 0,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ }
+ ];
+
+ return {
+ HeapDetailsRowDimension,
+ HeapDetailsTitleColumn,
+ AllocationCountColumn,
+ HEAP_DETAILS_COLUMN_RULES,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html
new file mode 100644
index 00000000000..5df80bb88b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane.html
@@ -0,0 +1,774 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/fixed_color_scheme.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_allocator_details_pane.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/color_legend.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/view_specific_brushing_state.html">
+
+<dom-module id='tr-ui-a-memory-dump-overview-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #label {
+ flex: 0 0 auto;
+ padding: 8px;
+
+ background-color: #eee;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ #label a {
+ font-weight: normal;
+ float: right;
+ }
+
+ #contents {
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ overflow: auto;
+ }
+
+ #info_text {
+ padding: 8px;
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ #table {
+ display: none; /* Hide until memory dumps are set. */
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-view-specific-brushing-state id="state"
+ view-id="analysis.memory_dump_overview_pane">
+ </tr-ui-b-view-specific-brushing-state>
+ <div id="label">Overview <a href="https://chromium.googlesource.com/chromium/src/+/master/docs/memory-infra">Help</a></div>
+ <div id="contents">
+ <div id="info_text">No memory memory dumps selected</div>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const MemoryColumnColorScheme = tr.b.MemoryColumnColorScheme;
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+
+ const PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX = '_bytes';
+
+ const DISPLAYED_SIZE_NUMERIC_NAME =
+ tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME;
+ const SOME_TIMESTAMPS_INFO_QUANTIFIER =
+ tr.ui.analysis.MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER;
+
+ // Unicode symbols used for memory cell info icons and messages.
+ const RIGHTWARDS_ARROW_WITH_HOOK = String.fromCharCode(0x21AA);
+ const RIGHTWARDS_ARROW_FROM_BAR = String.fromCharCode(0x21A6);
+ const GREATER_THAN_OR_EQUAL_TO = String.fromCharCode(0x2265);
+ const UNMARRIED_PARTNERSHIP_SYMBOL = String.fromCharCode(0x26AF);
+ const TRIGRAM_FOR_HEAVEN = String.fromCharCode(0x2630);
+
+ function lazyMap(list, fn, opt_this) {
+ opt_this = opt_this || this;
+ let result = undefined;
+ list.forEach(function(item, index) {
+ const value = fn.call(opt_this, item, index);
+ if (value === undefined) return;
+ if (result === undefined) {
+ result = new Array(list.length);
+ }
+ result[index] = value;
+ });
+ return result;
+ }
+
+ /** @constructor */
+ function ProcessNameColumn() {
+ tr.ui.analysis.TitleColumn.call(this, 'Process');
+ }
+
+ ProcessNameColumn.prototype = {
+ __proto__: tr.ui.analysis.TitleColumn.prototype,
+
+ formatTitle(row) {
+ if (row.contexts === undefined) {
+ return row.title; // Total row.
+ }
+ const titleEl = document.createElement('tr-ui-b-color-legend');
+ titleEl.label = row.title;
+ return titleEl;
+ }
+ };
+
+ /** @constructor */
+ function UsedMemoryColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.NumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ UsedMemoryColumn.COLOR =
+ MemoryColumnColorScheme.getColor('used_memory_column').toString();
+ UsedMemoryColumn.OLDER_COLOR =
+ MemoryColumnColorScheme.getColor('older_used_memory_column').toString();
+
+ UsedMemoryColumn.prototype = {
+ __proto__: tr.ui.analysis.NumericMemoryColumn.prototype,
+
+ get title() {
+ return tr.ui.b.createSpan({
+ textContent: this.name,
+ color: UsedMemoryColumn.COLOR
+ });
+ },
+
+ getFormattingContext(unit) {
+ return { unitPrefix: tr.b.UnitPrefixScale.BINARY.MEBI };
+ },
+
+ color(numerics, processMemoryDumps) {
+ return UsedMemoryColumn.COLOR;
+ },
+
+ getChildPaneBuilder(processMemoryDumps) {
+ if (processMemoryDumps === undefined) return undefined;
+
+ const vmRegions = lazyMap(processMemoryDumps, function(pmd) {
+ if (pmd === undefined) return undefined;
+ return pmd.mostRecentVmRegions;
+ });
+ if (vmRegions === undefined) return undefined;
+
+ return function() {
+ const pane = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ pane.vmRegions = vmRegions;
+ pane.aggregationMode = this.aggregationMode;
+ return pane;
+ }.bind(this);
+ }
+ };
+
+ /** @constructor */
+ function PeakMemoryColumn(name, cellPath, aggregationMode) {
+ UsedMemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ PeakMemoryColumn.prototype = {
+ __proto__: UsedMemoryColumn.prototype,
+
+ addInfos(numerics, processMemoryDumps, infos) {
+ if (processMemoryDumps === undefined) return; // Total row.
+
+ let resettableValueCount = 0;
+ let nonResettableValueCount = 0;
+ for (let i = 0; i < numerics.length; i++) {
+ if (numerics[i] === undefined) continue;
+ if (processMemoryDumps[i].arePeakResidentBytesResettable) {
+ resettableValueCount++;
+ } else {
+ nonResettableValueCount++;
+ }
+ }
+
+ if (resettableValueCount > 0 && nonResettableValueCount > 0) {
+ infos.push(tr.ui.analysis.createWarningInfo('Both resettable and ' +
+ 'non-resettable peak RSS values were provided by the process'));
+ } else if (resettableValueCount > 0) {
+ infos.push({
+ icon: RIGHTWARDS_ARROW_WITH_HOOK,
+ message: 'Peak RSS since previous memory dump.'
+ });
+ } else {
+ infos.push({
+ icon: RIGHTWARDS_ARROW_FROM_BAR,
+ message: 'Peak RSS since process startup. Finer grained ' +
+ 'peaks require a Linux kernel version ' +
+ GREATER_THAN_OR_EQUAL_TO + ' 4.0.'
+ });
+ }
+ }
+ };
+
+ /** @constructor */
+ function ByteStatColumn(name, cellPath, aggregationMode) {
+ UsedMemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ ByteStatColumn.prototype = {
+ __proto__: UsedMemoryColumn.prototype,
+
+ color(numerics, processMemoryDumps) {
+ if (processMemoryDumps === undefined) {
+ return UsedMemoryColumn.COLOR; // Total row.
+ }
+
+ const allOlderValues = processMemoryDumps.every(
+ function(processMemoryDump) {
+ if (processMemoryDump === undefined) return true;
+ return !processMemoryDump.hasOwnVmRegions;
+ });
+
+ // Show the cell in lighter blue if all values were older (i.e. none of
+ // the defined process memory dumps had own VM regions).
+ if (allOlderValues) {
+ return UsedMemoryColumn.OLDER_COLOR;
+ }
+ return UsedMemoryColumn.COLOR;
+ },
+
+ addInfos(numerics, processMemoryDumps, infos) {
+ if (processMemoryDumps === undefined) return; // Total row.
+
+ let olderValueCount = 0;
+ for (let i = 0; i < numerics.length; i++) {
+ const processMemoryDump = processMemoryDumps[i];
+ if (processMemoryDump !== undefined &&
+ !processMemoryDump.hasOwnVmRegions) {
+ olderValueCount++;
+ }
+ }
+
+ if (olderValueCount === 0) {
+ return; // There are no older values.
+ }
+
+ const infoQuantifier = olderValueCount < numerics.length ?
+ ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : /* some values are older */
+ ''; /* all values are older */
+
+ // Emit an info if there was at least one older value (i.e. at least one
+ // defined process memory dump did not have own VM regions).
+ infos.push({
+ message: 'Older value' + infoQuantifier +
+ ' (only heavy (purple) memory dumps contain memory maps).',
+ icon: UNMARRIED_PARTNERSHIP_SYMBOL
+ });
+ }
+ };
+
+ // Rules for constructing and sorting used memory columns.
+ UsedMemoryColumn.RULES = [
+ {
+ condition: 'Total resident',
+ importance: 10,
+ columnConstructor: UsedMemoryColumn
+ },
+ {
+ condition: 'Peak total resident',
+ importance: 9,
+ columnConstructor: PeakMemoryColumn
+ },
+ {
+ condition: 'PSS',
+ importance: 8,
+ columnConstructor: ByteStatColumn
+ },
+ {
+ condition: 'Private dirty',
+ importance: 7,
+ columnConstructor: ByteStatColumn
+ },
+ {
+ condition: 'Swapped',
+ importance: 6,
+ columnConstructor: ByteStatColumn
+ },
+ {
+ // All other columns.
+ importance: 0,
+ columnConstructor: UsedMemoryColumn
+ }
+ ];
+
+ // Map from ProcessMemoryDump totals fields to column names.
+ UsedMemoryColumn.TOTALS_MAP = {
+ 'residentBytes': 'Total resident',
+ 'peakResidentBytes': 'Peak total resident',
+ 'privateFootprintBytes': 'Private footprint',
+ };
+
+ // Map from ProcessMemoryDump platform-specific totals fields to column names.
+ UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP = {
+ 'vm': 'Total virtual',
+ 'swp': 'Swapped',
+ 'pc': 'Private clean',
+ 'pd': 'Private dirty',
+ 'sc': 'Shared clean',
+ 'sd': 'Shared dirty',
+ 'gpu_egl': 'GPU EGL',
+ 'gpu_egl_pss': 'GPU EGL PSS',
+ 'gpu_gl': 'GPU GL',
+ 'gpu_gl_pss': 'GPU GL PSS',
+ 'gpu_etc': 'GPU Other',
+ 'gpu_etc_pss': 'GPU Other PSS',
+ };
+
+ // Map from VMRegionByteStats field names to column names.
+ UsedMemoryColumn.BYTE_STAT_MAP = {
+ 'proportionalResident': 'PSS',
+ 'privateDirtyResident': 'Private dirty',
+ 'swapped': 'Swapped'
+ };
+
+ /** @constructor */
+ function AllocatorColumn(name, cellPath, aggregationMode) {
+ tr.ui.analysis.NumericMemoryColumn.call(
+ this, name, cellPath, aggregationMode);
+ }
+
+ AllocatorColumn.prototype = {
+ __proto__: tr.ui.analysis.NumericMemoryColumn.prototype,
+
+ get title() {
+ const titleEl = document.createElement('tr-ui-b-color-legend');
+ titleEl.label = this.name;
+ return titleEl;
+ },
+
+ getFormattingContext(unit) {
+ return { unitPrefix: tr.b.UnitPrefixScale.BINARY.MEBI };
+ },
+
+ addInfos(numerics, processMemoryDumps, infos) {
+ if (processMemoryDumps === undefined) return;
+
+ let heapDumpCount = 0;
+ let missingSizeCount = 0;
+
+ for (let i = 0; i < processMemoryDumps.length; i++) {
+ const processMemoryDump = processMemoryDumps[i];
+ if (processMemoryDump === undefined) continue;
+
+ const heapDumps = processMemoryDump.heapDumps;
+ if (heapDumps !== undefined && heapDumps[this.name] !== undefined) {
+ heapDumpCount++;
+ }
+ const allocatorDump =
+ processMemoryDump.getMemoryAllocatorDumpByFullName(this.name);
+
+ if (allocatorDump !== undefined &&
+ allocatorDump.numerics[DISPLAYED_SIZE_NUMERIC_NAME] === undefined) {
+ missingSizeCount++;
+ }
+ }
+
+ // Emit a heap dump info if at least one of the process memory dumps has
+ // a heap dump associated with this allocator.
+ if (heapDumpCount > 0) {
+ const infoQuantifier = heapDumpCount < numerics.length ?
+ ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : '';
+ infos.push({
+ message: 'Heap dump provided' + infoQuantifier + '.',
+ icon: TRIGRAM_FOR_HEAVEN
+ });
+ }
+
+ // Emit a warning if this allocator did not provide size in at least one
+ // of the process memory dumps.
+ if (missingSizeCount > 0) {
+ const infoQuantifier = missingSizeCount < numerics.length ?
+ ' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : '';
+ infos.push(tr.ui.analysis.createWarningInfo(
+ 'Size was not provided' + infoQuantifier + '.'));
+ }
+ },
+
+ getChildPaneBuilder(processMemoryDumps) {
+ if (processMemoryDumps === undefined) return undefined;
+
+ const memoryAllocatorDumps = lazyMap(processMemoryDumps, function(pmd) {
+ if (pmd === undefined) return undefined;
+ return pmd.getMemoryAllocatorDumpByFullName(this.name);
+ }, this);
+ if (memoryAllocatorDumps === undefined) return undefined;
+
+ const heapDumps = lazyMap(processMemoryDumps, function(pmd) {
+ if (pmd === undefined || pmd.heapDumps === undefined) return undefined;
+ return pmd.heapDumps[this.name];
+ }, this);
+
+ return function() {
+ const pane = document.createElement(
+ 'tr-ui-a-memory-dump-allocator-details-pane');
+ pane.memoryAllocatorDumps = memoryAllocatorDumps;
+ pane.heapDumps = heapDumps;
+ pane.aggregationMode = this.aggregationMode;
+ return pane;
+ }.bind(this);
+ }
+ };
+
+ /** @constructor */
+ function TracingColumn(name, cellPath, aggregationMode) {
+ AllocatorColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ TracingColumn.COLOR =
+ MemoryColumnColorScheme.getColor('tracing_memory_column').toString();
+
+ TracingColumn.prototype = {
+ __proto__: AllocatorColumn.prototype,
+
+ get title() {
+ return tr.ui.b.createSpan({
+ textContent: this.name,
+ color: TracingColumn.COLOR
+ });
+ },
+
+ color(numerics, processMemoryDumps) {
+ return TracingColumn.COLOR;
+ }
+ };
+
+ // Rules for constructing and sorting allocator columns.
+ AllocatorColumn.RULES = [
+ {
+ condition: 'tracing',
+ importance: 0,
+ columnConstructor: TracingColumn
+ },
+ {
+ // All other columns.
+ importance: 1,
+ columnConstructor: AllocatorColumn
+ }
+ ];
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-overview-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.processMemoryDumps_ = undefined;
+ this.aggregationMode_ = undefined;
+ },
+
+ ready() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.CELL;
+ this.$.table.addEventListener('selection-changed',
+ function(tableEvent) {
+ tableEvent.stopPropagation();
+ this.changeChildPane_();
+ }.bind(this));
+ },
+
+ /**
+ * Sets the process memory dumps and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronological list of dictionaries
+ * mapping process IDs to process memory dumps. Example:
+ *
+ * [
+ * {
+ * // PMDs at timestamp 1.
+ * 42: tr.model.ProcessMemoryDump {}
+ * },
+ * {
+ * // PMDs at timestamp 2.
+ * 42: tr.model.ProcessMemoryDump {},
+ * 89: tr.model.ProcessMemoryDump {}
+ * }
+ * ]
+ */
+ set processMemoryDumps(processMemoryDumps) {
+ this.processMemoryDumps_ = processMemoryDumps;
+ this.scheduleRebuild_();
+ },
+
+ get processMemoryDumps() {
+ return this.processMemoryDumps_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ get selectedMemoryCell() {
+ if (this.processMemoryDumps_ === undefined ||
+ this.processMemoryDumps_.length === 0) {
+ return undefined;
+ }
+
+ const selectedTableRow = this.$.table.selectedTableRow;
+ if (!selectedTableRow) return undefined;
+
+ const selectedColumnIndex = this.$.table.selectedColumnIndex;
+ if (selectedColumnIndex === undefined) return undefined;
+
+ const selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
+ const selectedMemoryCell = selectedColumn.cell(selectedTableRow);
+ return selectedMemoryCell;
+ },
+
+ changeChildPane_() {
+ this.storeSelection_();
+ this.childPaneBuilder = this.determineChildPaneBuilderFromSelection_();
+ },
+
+ determineChildPaneBuilderFromSelection_() {
+ if (this.processMemoryDumps_ === undefined ||
+ this.processMemoryDumps_.length === 0) {
+ return undefined;
+ }
+
+ const selectedTableRow = this.$.table.selectedTableRow;
+ if (!selectedTableRow) return undefined;
+
+ const selectedColumnIndex = this.$.table.selectedColumnIndex;
+ if (selectedColumnIndex === undefined) return undefined;
+ const selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
+
+ return selectedColumn.getChildPaneBuilder(selectedTableRow.contexts);
+ },
+
+ onRebuild_() {
+ if (this.processMemoryDumps_ === undefined ||
+ this.processMemoryDumps_.length === 0) {
+ // Show the info text (hide the table).
+ this.$.info_text.style.display = 'block';
+ this.$.table.style.display = 'none';
+
+ this.$.table.clear();
+ this.$.table.rebuild();
+ return;
+ }
+
+ // Show the table (hide the info text).
+ this.$.info_text.style.display = 'none';
+ this.$.table.style.display = 'block';
+
+ const rows = this.createRows_();
+ const columns = this.createColumns_(rows);
+ const footerRows = this.createFooterRows_(rows, columns);
+
+ this.$.table.tableRows = rows;
+ this.$.table.footerRows = footerRows;
+ this.$.table.tableColumns = columns;
+ this.$.table.rebuild();
+
+ this.restoreSelection_();
+ },
+
+ createRows_() {
+ // Timestamp (list index) -> Process ID (dict key) -> PMD.
+ const timeToPidToProcessMemoryDump = this.processMemoryDumps_;
+
+ // Process ID (dict key) -> Timestamp (list index) -> PMD or undefined.
+ const pidToTimeToProcessMemoryDump = tr.b.invertArrayOfDicts(
+ timeToPidToProcessMemoryDump);
+
+ // Process (list index) -> Component (dict key) -> Cell.
+ const rows = [];
+ for (const [pid, timeToDump] of
+ Object.entries(pidToTimeToProcessMemoryDump)) {
+ // Get the process associated with the dumps. We can use any defined
+ // process memory dump in timeToDump since they all have the same
+ // pid.
+ const process = timeToDump.find(x => x).process;
+
+ // Used memory (total resident, PSS, ...).
+ const usedMemoryCells = tr.ui.analysis.createCells(timeToDump,
+ function(dump) {
+ const sizes = {};
+
+ const totals = dump.totals;
+ if (totals !== undefined) {
+ // Common totals.
+ for (const [totalName, cellName] of
+ Object.entries(UsedMemoryColumn.TOTALS_MAP)) {
+ const total = totals[totalName];
+ if (total === undefined) continue;
+ sizes[cellName] = new Scalar(
+ sizeInBytes_smallerIsBetter, total);
+ }
+
+ // Platform-specific totals (e.g. private resident on Mac).
+ const platformSpecific = totals.platformSpecific;
+ if (platformSpecific !== undefined) {
+ for (const [name, size] of Object.entries(platformSpecific)) {
+ let newName = name;
+ if (UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP[name] ===
+ undefined) {
+ // Change raw OS-specific total name to a friendly
+ // column title (e.g. 'private_bytes' -> 'Private').
+ if (name.endsWith(PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX)) {
+ newName = name.substring(0, name.length -
+ PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX.length);
+ }
+ newName = newName.replace('_', ' ').trim();
+ newName =
+ newName.charAt(0).toUpperCase() + newName.slice(1);
+ } else {
+ newName =
+ UsedMemoryColumn.PLATFORM_SPECIFIC_TOTALS_MAP[name];
+ }
+ sizes[newName] = new Scalar(
+ sizeInBytes_smallerIsBetter, size);
+ }
+ }
+ }
+
+ // VM regions byte stats.
+ const vmRegions = dump.mostRecentVmRegions;
+ if (vmRegions !== undefined) {
+ for (const [byteStatName, cellName] of
+ Object.entries(UsedMemoryColumn.BYTE_STAT_MAP)) {
+ const byteStat = vmRegions.byteStats[byteStatName];
+ if (byteStat === undefined) continue;
+ sizes[cellName] = new Scalar(
+ sizeInBytes_smallerIsBetter, byteStat);
+ }
+ }
+
+ return sizes;
+ });
+
+ // Allocator memory (v8, oilpan, ...).
+ const allocatorCells = tr.ui.analysis.createCells(timeToDump,
+ function(dump) {
+ const memoryAllocatorDumps = dump.memoryAllocatorDumps;
+ if (memoryAllocatorDumps === undefined) return undefined;
+
+ const sizes = {};
+ memoryAllocatorDumps.forEach(function(allocatorDump) {
+ let rootDisplayedSizeNumeric = allocatorDump.numerics[
+ DISPLAYED_SIZE_NUMERIC_NAME];
+ if (rootDisplayedSizeNumeric === undefined) {
+ rootDisplayedSizeNumeric =
+ new Scalar(sizeInBytes_smallerIsBetter, 0);
+ }
+ sizes[allocatorDump.fullName] = rootDisplayedSizeNumeric;
+ });
+ return sizes;
+ });
+
+ rows.push({
+ title: process.userFriendlyName,
+ contexts: timeToDump,
+ usedMemoryCells,
+ allocatorCells
+ });
+ }
+ return rows;
+ },
+
+ createFooterRows_(rows, columns) {
+ // Add a 'Total' row if there are at least two process memory dumps.
+ if (rows.length <= 1) return [];
+
+ const totalRow = {title: 'Total'};
+ tr.ui.analysis.aggregateTableRowCells(totalRow, rows, columns);
+
+ return [totalRow];
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new ProcessNameColumn();
+ titleColumn.width = '200px';
+
+ const usedMemorySizeColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'usedMemoryCells',
+ aggregationMode: this.aggregationMode_,
+ rules: UsedMemoryColumn.RULES
+ });
+
+ const allocatorSizeColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'allocatorCells',
+ aggregationMode: this.aggregationMode_,
+ rules: AllocatorColumn.RULES
+ });
+
+ const sizeColumns = usedMemorySizeColumns.concat(allocatorSizeColumns);
+ tr.ui.analysis.MemoryColumn.spaceEqually(sizeColumns);
+
+ const columns = [titleColumn].concat(sizeColumns);
+ return columns;
+ },
+
+ storeSelection_() {
+ let selectedRowTitle;
+ const selectedRow = this.$.table.selectedTableRow;
+ if (selectedRow !== undefined) {
+ selectedRowTitle = selectedRow.title;
+ }
+
+ let selectedColumnName;
+ const selectedColumnIndex = this.$.table.selectedColumnIndex;
+ if (selectedColumnIndex !== undefined) {
+ const selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
+ selectedColumnName = selectedColumn.name;
+ }
+
+ this.$.state.set(
+ {rowTitle: selectedRowTitle, columnName: selectedColumnName});
+ },
+
+ restoreSelection_() {
+ const settings = this.$.state.get();
+ if (settings === undefined || settings.rowTitle === undefined ||
+ settings.columnName === undefined) {
+ return;
+ }
+
+ const selectedColumnIndex = this.$.table.tableColumns.findIndex(
+ col => col.name === settings.columnName);
+ if (selectedColumnIndex === -1) return;
+
+ const selectedRowTitle = settings.rowTitle;
+ const selectedRow = this.$.table.tableRows.find(
+ row => row.title === selectedRowTitle);
+ if (selectedRow === undefined) return;
+
+ this.$.table.selectedTableRow = selectedRow;
+ this.$.table.selectedColumnIndex = selectedColumnIndex;
+ }
+ });
+
+ return {
+ // All exports are for testing only.
+ ProcessNameColumn,
+ UsedMemoryColumn,
+ PeakMemoryColumn,
+ ByteStatColumn,
+ AllocatorColumn,
+ TracingColumn,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html
new file mode 100644
index 00000000000..80127e020a8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_overview_pane_test.html
@@ -0,0 +1,840 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/heap_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_overview_pane.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const MemoryAllocatorDump = tr.model.MemoryAllocatorDump;
+ const HeapDump = tr.model.HeapDump;
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const checkColor = tr.ui.analysis.checkColor;
+ const checkColumns = tr.ui.analysis.checkColumns;
+ const checkColumnInfosAndColor = tr.ui.analysis.checkColumnInfosAndColor;
+ const convertToProcessMemoryDumps =
+ tr.ui.analysis.convertToProcessMemoryDumps;
+ const extractProcessMemoryDumps = tr.ui.analysis.extractProcessMemoryDumps;
+ const extractVmRegions = tr.ui.analysis.extractVmRegions;
+ const extractMemoryAllocatorDumps =
+ tr.ui.analysis.extractMemoryAllocatorDumps;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const ProcessNameColumn = tr.ui.analysis.ProcessNameColumn;
+ const UsedMemoryColumn = tr.ui.analysis.UsedMemoryColumn;
+ const PeakMemoryColumn = tr.ui.analysis.PeakMemoryColumn;
+ const ByteStatColumn = tr.ui.analysis.ByteStatColumn;
+ const AllocatorColumn = tr.ui.analysis.AllocatorColumn;
+ const TracingColumn = tr.ui.analysis.TracingColumn;
+
+ function spanMatcher(expectedTitle) {
+ return function(actualTitle) {
+ assert.instanceOf(actualTitle, HTMLElement);
+ assert.strictEqual(actualTitle.tagName, 'SPAN');
+ assert.strictEqual(Polymer.dom(actualTitle).textContent, expectedTitle);
+ };
+ }
+
+ function colorLegendMatcher(expectedTitle) {
+ return function(actualTitle) {
+ assert.instanceOf(actualTitle, HTMLElement);
+ assert.strictEqual(actualTitle.tagName, 'TR-UI-B-COLOR-LEGEND');
+ assert.strictEqual(actualTitle.label, expectedTitle);
+ };
+ }
+
+ const EXPECTED_COLUMNS = [
+ { title: 'Process', type: ProcessNameColumn, noAggregation: true },
+ { title: spanMatcher('Total resident'), type: UsedMemoryColumn },
+ { title: spanMatcher('Peak total resident'), type: PeakMemoryColumn },
+ { title: spanMatcher('PSS'), type: ByteStatColumn },
+ { title: spanMatcher('Private dirty'), type: ByteStatColumn },
+ { title: spanMatcher('Swapped'), type: ByteStatColumn },
+ { title: spanMatcher('Private'), type: UsedMemoryColumn },
+ { title: spanMatcher('Private footprint'), type: UsedMemoryColumn },
+ { title: colorLegendMatcher('blink'), type: AllocatorColumn },
+ { title: colorLegendMatcher('gpu'), type: AllocatorColumn },
+ { title: colorLegendMatcher('malloc'), type: AllocatorColumn },
+ { title: colorLegendMatcher('oilpan'), type: AllocatorColumn },
+ { title: colorLegendMatcher('v8'), type: AllocatorColumn },
+ { title: spanMatcher('tracing'), type: TracingColumn }
+ ];
+
+ function checkRow(columns, row, expectedTitle, expectedSizes,
+ expectedContexts) {
+ // Check title.
+ const formattedTitle = columns[0].formatTitle(row);
+ if (typeof expectedTitle === 'function') {
+ expectedTitle(formattedTitle);
+ } else {
+ assert.strictEqual(formattedTitle, expectedTitle);
+ }
+
+ // Check all sizes. The first assert below is a test sanity check.
+ assert.lengthOf(expectedSizes, columns.length - 1 /* all except title */);
+ for (let i = 0; i < expectedSizes.length; i++) {
+ checkSizeNumericFields(row, columns[i + 1], expectedSizes[i]);
+ }
+
+ // There should be no row nesting on the overview pane.
+ assert.isUndefined(row.subRows);
+
+ if (expectedContexts) {
+ assert.deepEqual(Array.from(row.contexts), expectedContexts);
+ } else {
+ assert.isUndefined(row.contexts);
+ }
+ }
+
+ function checkRows(columns, actualRows, expectedRows) {
+ if (expectedRows === undefined) {
+ assert.isUndefined(actualRows);
+ return;
+ }
+ assert.lengthOf(actualRows, expectedRows.length);
+ for (let i = 0; i < expectedRows.length; i++) {
+ const actualRow = actualRows[i];
+ const expectedRow = expectedRows[i];
+ checkRow(columns, actualRow, expectedRow.title, expectedRow.sizes,
+ expectedRow.contexts);
+ }
+ }
+
+ function checkSpanWithColor(span, expectedText, expectedColor) {
+ assert.strictEqual(span.tagName, 'SPAN');
+ assert.strictEqual(Polymer.dom(span).textContent, expectedText);
+ checkColor(span.style.color, expectedColor);
+ }
+
+ function checkColorLegend(legend, expectedLabel) {
+ assert.strictEqual(legend.tagName, 'TR-UI-B-COLOR-LEGEND');
+ assert.strictEqual(legend.label, expectedLabel);
+ }
+
+ function createAndCheckMemoryDumpOverviewPane(
+ test, processMemoryDumps, expectedRows, expectedFooterRows,
+ aggregationMode) {
+ const viewEl =
+ tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane');
+ viewEl.processMemoryDumps = processMemoryDumps;
+ viewEl.aggregationMode = aggregationMode;
+ viewEl.rebuild();
+ test.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ assert.isUndefined(viewEl.createChildPane());
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, aggregationMode);
+ const rows = table.tableRows;
+
+ checkRows(columns, table.tableRows, expectedRows);
+ checkRows(columns, table.footerRows, expectedFooterRows);
+ }
+
+ const FIELD = 1 << 0;
+ const DUMP = 1 << 1;
+
+ function checkOverviewColumnInfosAndColor(column, fieldAndDumpMask,
+ dumpCreatedCallback, expectedInfos, expectedColorReservedName) {
+ const fields = fieldAndDumpMask.map(function(mask, index) {
+ return mask & FIELD ?
+ new Scalar(sizeInBytes_smallerIsBetter, 1024 + 32 * index) :
+ undefined;
+ });
+
+ let contexts;
+ if (dumpCreatedCallback === undefined) {
+ contexts = undefined;
+ } else {
+ tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ fieldAndDumpMask.forEach(function(mask, i) {
+ const timestamp = 10 + i;
+ const gmd = addGlobalMemoryDump(model, {ts: timestamp});
+ if (mask & DUMP) {
+ const pmd = addProcessMemoryDump(gmd, process, {ts: timestamp});
+ dumpCreatedCallback(pmd, mask);
+ }
+ });
+ contexts = model.globalMemoryDumps.map(function(gmd) {
+ return gmd.processMemoryDumps[1];
+ });
+ });
+ }
+
+ checkColumnInfosAndColor(
+ column, fields, contexts, expectedInfos, expectedColorReservedName);
+ }
+
+ test('colorsAreDefined', function() {
+ // We use these constants in the code and the tests so here we guard
+ // against them being undefined and causing all the tests to still
+ // pass while the we end up with no colors.
+ assert.isDefined(UsedMemoryColumn.COLOR);
+ assert.isDefined(UsedMemoryColumn.OLDER_COLOR);
+ assert.isDefined(TracingColumn.COLOR);
+ });
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-overview-pane', 'processMemoryDumps',
+ function(viewEl) {
+ // Check that the info text is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.info_text));
+ assert.isFalse(isElementDisplayed(viewEl.$.table));
+ });
+ });
+
+ test('instantiate_singleGlobalMemoryDump', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ [tr.ui.analysis.createSingleTestGlobalMemoryDump()]);
+ createAndCheckMemoryDumpOverviewPane(this,
+ processMemoryDumps,
+ [ // Table rows.
+ {
+ title: colorLegendMatcher('Process 1'),
+ sizes: [[29884416], undefined, [9437184], [5767168], undefined,
+ undefined, undefined, undefined, undefined, [7340032], undefined,
+ undefined, [2097152]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 1)
+ },
+ {
+ title: colorLegendMatcher('Process 2'),
+ sizes: [[17825792], [39845888], [18350080], [0], [32], [8912896],
+ [15728640], [7340032], [0], [1048576], [1], [5242880],
+ [1572864]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 2)
+ },
+ {
+ title: colorLegendMatcher('Process 4'),
+ sizes: [undefined, [17825792], undefined, undefined, undefined,
+ undefined, undefined, undefined, undefined, undefined,
+ undefined, undefined, undefined],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 4)
+ }
+ ],
+ [ // Footer rows.
+ {
+ title: 'Total',
+ sizes: [[47710208], [57671680], [27787264], [5767168], [32],
+ [8912896], [15728640], [7340032], [0], [8388608], [1],
+ [5242880], [3670016]],
+ contexts: undefined
+ }
+ ],
+ undefined /* no aggregation */);
+ });
+
+ test('instantiate_multipleGlobalMemoryDumps', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps());
+ createAndCheckMemoryDumpOverviewPane(this,
+ processMemoryDumps,
+ [ // Table rows.
+ {
+ title: colorLegendMatcher('Process 1'),
+ sizes: [[31457280, 29884416, undefined], undefined,
+ [10485760, 9437184, undefined], [8388608, 5767168, undefined],
+ undefined, undefined, undefined, undefined, undefined,
+ [undefined, 7340032, undefined], undefined, undefined,
+ [undefined, 2097152, undefined]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 1)
+ },
+ {
+ title: colorLegendMatcher('Process 2'),
+ sizes: [[19398656, 17825792, 15728640],
+ [40370176, 39845888, 40894464], [18350080, 18350080, 18350080],
+ [0, 0, -2621440], [32, 32, 64], [10485760, 8912896, 7340032],
+ [15728640, 15728640, 15728640], [undefined, 7340032, 6291456],
+ [undefined, 0, 1048576], [2097152, 1048576, 786432],
+ [undefined, 1, undefined], [5242880, 5242880, 5767168],
+ [1048576, 1572864, 2097152]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 2)
+ },
+ {
+ title: colorLegendMatcher('Process 3'),
+ sizes: [undefined, undefined, undefined, undefined, undefined,
+ undefined, undefined, undefined, undefined, undefined,
+ [2147483648, undefined, 1073741824],
+ [1073741824, undefined, 2147483648], undefined],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 3)
+ },
+ {
+ title: colorLegendMatcher('Process 4'),
+ sizes: [undefined, [undefined, 17825792, 17825792], undefined,
+ undefined, undefined, undefined, undefined, undefined,
+ undefined, undefined, undefined, undefined, undefined],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 4)
+ }
+ ],
+ [ // Footer rows.
+ {
+ title: 'Total',
+ sizes: [[50855936, 47710208, 15728640],
+ [40370176, 57671680, 58720256], [28835840, 27787264, 18350080],
+ [8388608, 5767168, -2621440], [32, 32, 64],
+ [10485760, 8912896, 7340032], [15728640, 15728640, 15728640],
+ [undefined, 7340032, 6291456], [undefined, 0, 1048576],
+ [2097152, 8388608, 786432], [2147483648, 1, 1073741824],
+ [1078984704, 5242880, 2153250816],
+ [1048576, 3670016, 2097152]],
+ contexts: undefined
+ }
+ ],
+ AggregationMode.DIFF);
+ });
+
+ test('instantiate_singleProcessMemoryDump', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ [tr.ui.analysis.createSingleTestProcessMemoryDump()]);
+ createAndCheckMemoryDumpOverviewPane(this,
+ processMemoryDumps,
+ [ // Table rows.
+ {
+ title: colorLegendMatcher('Process 2'),
+ sizes: [[17825792], [39845888], [18350080], [0], [32], [8912896],
+ [15728640], [7340032], [0], [1048576], [1], [5242880],
+ [1572864]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 2)
+ }
+ ],
+ [] /* footer rows */,
+ undefined /* no aggregation */);
+ });
+
+ test('instantiate_multipleProcessMemoryDumps', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ tr.ui.analysis.createMultipleTestProcessMemoryDumps());
+ createAndCheckMemoryDumpOverviewPane(this,
+ processMemoryDumps,
+ [ // Table rows.
+ {
+ title: colorLegendMatcher('Process 2'),
+ sizes: [[19398656, 17825792, 15728640],
+ [40370176, 39845888, 40894464], [18350080, 18350080, 18350080],
+ [0, 0, -2621440], [32, 32, 64], [10485760, 8912896, 7340032],
+ [15728640, 15728640, 15728640], [undefined, 7340032, 6291456],
+ [undefined, 0, 1048576], [2097152, 1048576, 786432],
+ [undefined, 1, undefined], [5242880, 5242880, 5767168],
+ [1048576, 1572864, 2097152]],
+ contexts: extractProcessMemoryDumps(processMemoryDumps, 2)
+ }
+ ],
+ [] /* footer rows */,
+ AggregationMode.MAX);
+ });
+
+ test('selection', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps());
+
+ const viewEl =
+ tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane');
+ viewEl.processMemoryDumps = processMemoryDumps;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ const table = viewEl.$.table;
+
+ // Simulate clicking on the 'malloc' cell of the second process.
+ table.selectedTableRow = table.tableRows[1];
+ table.selectedColumnIndex = 10;
+ assert.lengthOf(viewEl.requestedChildPanes, 2);
+ let lastChildPane = viewEl.requestedChildPanes[1];
+ assert.strictEqual(
+ lastChildPane.tagName, 'TR-UI-A-MEMORY-DUMP-ALLOCATOR-DETAILS-PANE');
+ assert.strictEqual(lastChildPane.aggregationMode, AggregationMode.DIFF);
+ assert.deepEqual(lastChildPane.memoryAllocatorDumps,
+ extractMemoryAllocatorDumps(processMemoryDumps, 2, 'malloc'));
+
+ // Simulate clicking on the 'Oilpan' cell of the second process.
+ table.selectedColumnIndex = 10;
+ assert.lengthOf(viewEl.requestedChildPanes, 3);
+ lastChildPane = viewEl.requestedChildPanes[2];
+ assert.isUndefined(viewEl.lastChildPane);
+ });
+
+ test('memory', function() {
+ const processMemoryDumps = convertToProcessMemoryDumps(
+ tr.ui.analysis.createMultipleTestGlobalMemoryDumps());
+ const containerEl = document.createElement('div');
+ containerEl.brushingStateController =
+ new tr.c.BrushingStateController(undefined);
+
+ function simulateView(pids, aggregationMode,
+ expectedSelectedCellFieldValues, expectedSelectedRowTitle,
+ expectedSelectedColumnIndex, callback) {
+ const viewEl =
+ tr.ui.analysis.createTestPane('tr-ui-a-memory-dump-overview-pane');
+ const table = viewEl.$.table;
+ Polymer.dom(containerEl).textContent = '';
+ Polymer.dom(containerEl).appendChild(viewEl);
+
+ const displayedProcessMemoryDumps = processMemoryDumps.map(
+ function(memoryDumps) {
+ const result = {};
+ for (const [pid, pmd] of Object.entries(memoryDumps)) {
+ if (pids.includes(pmd.process.pid)) result[pid] = pmd;
+ }
+ return result;
+ });
+ viewEl.processMemoryDumps = displayedProcessMemoryDumps;
+ viewEl.aggregationMode = aggregationMode;
+ viewEl.rebuild();
+
+ if (expectedSelectedCellFieldValues === undefined) {
+ assert.isUndefined(viewEl.childPaneBuilder);
+ } else {
+ checkSizeNumericFields(table.selectedTableRow,
+ table.tableColumns[table.selectedColumnIndex],
+ expectedSelectedCellFieldValues);
+ }
+
+ assert.strictEqual(
+ table.selectedColumnIndex, expectedSelectedColumnIndex);
+ if (expectedSelectedRowTitle === undefined) {
+ assert.isUndefined(table.selectedTableRow);
+ } else {
+ assert.strictEqual(
+ table.selectedTableRow.title, expectedSelectedRowTitle);
+ }
+
+ callback(viewEl, viewEl.$.table);
+ }
+
+ simulateView(
+ [1, 2, 3, 4], // All processes.
+ AggregationMode.DIFF,
+ undefined, undefined, undefined, // No cell should be selected.
+ function(view, table) {
+ assert.isUndefined(view.createChildPane());
+
+ // Select the 'PSS' column of the second process.
+ table.selectedTableRow = table.tableRows[1];
+ table.selectedColumnIndex = 3;
+ });
+
+ simulateView(
+ [2, 3],
+ AggregationMode.MAX,
+ [18350080, 18350080, 18350080], 'Process 2', 3, /* PSS */
+ function(view, table) {
+ const childPane = view.createChildPane();
+ assert.strictEqual(
+ childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE');
+ assert.deepEqual(Array.from(childPane.vmRegions),
+ extractVmRegions(processMemoryDumps, 2));
+ assert.strictEqual(childPane.aggregationMode, AggregationMode.MAX);
+ });
+
+ simulateView(
+ [3],
+ undefined, /* No aggregation */
+ undefined, undefined, undefined, // No cell selected.
+ function(view, table) {
+ assert.isUndefined(view.createChildPane());
+ });
+
+ simulateView(
+ [1, 2, 3, 4],
+ AggregationMode.DIFF,
+ [18350080, 18350080, 18350080], 'Process 2', 3, /* PSS */
+ function(view, table) {
+ const childPane = view.createChildPane();
+ assert.strictEqual(
+ childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE');
+ assert.deepEqual(Array.from(childPane.vmRegions),
+ extractVmRegions(processMemoryDumps, 2));
+ assert.strictEqual(childPane.aggregationMode, AggregationMode.DIFF);
+
+ // Select the 'v8' column of the first process (empty cell).
+ table.selectedTableRow = table.tableRows[0];
+ table.selectedColumnIndex = 11;
+ });
+
+ simulateView(
+ [1],
+ undefined, /* No aggregation */
+ undefined, undefined, undefined, // No cell should selected.
+ function(view, table) {
+ assert.isUndefined(view.createChildPane());
+
+ // Select 'Total resident' column of the first process.
+ table.selectedTableRow = table.tableRows[0];
+ table.selectedColumnIndex = 1;
+ });
+
+ simulateView(
+ [1, 2, 3, 4],
+ AggregationMode.MAX,
+ [31457280, 29884416, undefined], 'Process 1', 1, /* Total resident */
+ function(view, table) {
+ const childPane = view.createChildPane();
+ assert.strictEqual(
+ childPane.tagName, 'TR-UI-A-MEMORY-DUMP-VM-REGIONS-DETAILS-PANE');
+ assert.deepEqual(Array.from(childPane.vmRegions),
+ extractVmRegions(processMemoryDumps, 1));
+ assert.strictEqual(childPane.aggregationMode, AggregationMode.MAX);
+ });
+ });
+
+ test('processNameColumn_formatTitle', function() {
+ const c = new ProcessNameColumn();
+
+ // With context (total row).
+ assert.strictEqual(c.formatTitle({
+ title: 'Total',
+ usedMemoryCells: {}
+ }), 'Total');
+
+ // Without context (process row).
+ const title = c.formatTitle({
+ title: 'Process 1',
+ usedMemoryCells: {},
+ contexts: [tr.ui.analysis.createSingleTestProcessMemoryDump()]
+ });
+ checkColorLegend(title, 'Process 1');
+ });
+
+ test('usedMemoryColumn', function() {
+ const c = new UsedMemoryColumn('Private', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ checkSpanWithColor(c.title, 'Private',
+ UsedMemoryColumn.COLOR /* blue (column title) */);
+ checkColor(c.color(undefined /* contexts */),
+ UsedMemoryColumn.COLOR /* blue (column cells) */);
+ });
+
+ test('peakMemoryColumn', function() {
+ const c = new PeakMemoryColumn('Peak', 'bytes', (x => x),
+ AggregationMode.MAX);
+ checkSpanWithColor(c.title, 'Peak',
+ UsedMemoryColumn.COLOR /* blue (column title) */);
+ checkColor(c.color(undefined) /* contexts */,
+ UsedMemoryColumn.COLOR /* blue (column cells) */);
+
+ const RESETTABLE_PEAK = 1 << 2;
+ const NON_RESETTABLE_PEAK = 1 << 3;
+ function checkPeakColumnInfosAndColor(fieldAndDumpMask, expectedInfos) {
+ checkOverviewColumnInfosAndColor(c,
+ fieldAndDumpMask,
+ function(pmd, mask) {
+ if (mask & RESETTABLE_PEAK) {
+ assert.strictEqual(
+ mask & NON_RESETTABLE_PEAK, 0); // Test sanity check.
+ pmd.arePeakResidentBytesResettable = true;
+ } else if (mask & NON_RESETTABLE_PEAK) {
+ pmd.arePeakResidentBytesResettable = false;
+ }
+ },
+ expectedInfos,
+ UsedMemoryColumn.COLOR);
+ }
+
+ // No context.
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ UsedMemoryColumn.COLOR /* blue color */);
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD, FIELD, 0, FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ UsedMemoryColumn.COLOR /* blue color */);
+
+ // All resettable.
+ const EXPECTED_RESETTABLE_INFO = {
+ icon: '\u21AA',
+ message: 'Peak RSS since previous memory dump.'
+ };
+ checkPeakColumnInfosAndColor([
+ FIELD | DUMP | RESETTABLE_PEAK
+ ], [EXPECTED_RESETTABLE_INFO]);
+ checkPeakColumnInfosAndColor([
+ FIELD | DUMP | RESETTABLE_PEAK,
+ DUMP /* ignored because there's no field */,
+ 0,
+ FIELD | DUMP | RESETTABLE_PEAK
+ ], [EXPECTED_RESETTABLE_INFO]);
+
+ // All non-resettable.
+ const EXPECTED_NON_RESETTABLE_INFO = {
+ icon: '\u21A6',
+ message: 'Peak RSS since process startup. Finer grained peaks require ' +
+ 'a Linux kernel version \u2265 4.0.'
+ };
+ checkPeakColumnInfosAndColor([
+ FIELD | DUMP | NON_RESETTABLE_PEAK
+ ], [EXPECTED_NON_RESETTABLE_INFO]);
+ checkPeakColumnInfosAndColor([
+ 0,
+ DUMP | RESETTABLE_PEAK /* ignored because there's no field */,
+ FIELD | DUMP | NON_RESETTABLE_PEAK,
+ FIELD | DUMP | NON_RESETTABLE_PEAK
+ ], [EXPECTED_NON_RESETTABLE_INFO]);
+
+ // Combination (warning).
+ const EXPECTED_COMBINATION_INFO = {
+ icon: '\u26A0',
+ message: 'Both resettable and non-resettable peak RSS values were ' +
+ 'provided by the process',
+ color: 'red'
+ };
+ checkPeakColumnInfosAndColor([
+ FIELD | DUMP | NON_RESETTABLE_PEAK,
+ 0,
+ FIELD | DUMP | RESETTABLE_PEAK,
+ 0
+ ], [EXPECTED_COMBINATION_INFO]);
+ });
+
+ test('byteStatColumn', function() {
+ const c = new ByteStatColumn('Stat', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ checkSpanWithColor(c.title, 'Stat',
+ UsedMemoryColumn.COLOR /* blue (column title) */);
+
+ const HAS_OWN_VM_REGIONS = 1 << 2;
+ function checkByteStatColumnInfosAndColor(
+ fieldAndDumpMask, expectedInfos, expectedIsOlderColor) {
+ checkOverviewColumnInfosAndColor(c,
+ fieldAndDumpMask,
+ function(pmd, mask) {
+ if (mask & HAS_OWN_VM_REGIONS) {
+ pmd.vmRegions = [];
+ }
+ },
+ expectedInfos,
+ expectedIsOlderColor ?
+ UsedMemoryColumn.OLDER_COLOR /* light blue */ :
+ UsedMemoryColumn.COLOR /* blue color */);
+ }
+
+ const EXPECTED_ALL_OLDER_VALUES = {
+ icon: '\u26AF',
+ message: 'Older value (only heavy (purple) memory dumps contain ' +
+ 'memory maps).'
+ };
+ const EXPECTED_SOME_OLDER_VALUES = {
+ icon: '\u26AF',
+ message: 'Older value at some selected timestamps (only heavy ' +
+ '(purple) memory dumps contain memory maps).'
+ };
+
+ // No context.
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ UsedMemoryColumn.COLOR /* blue color */);
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD, FIELD, 0, FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ UsedMemoryColumn.COLOR /* blue color */);
+
+ // All process memory dumps have own VM regions.
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP | HAS_OWN_VM_REGIONS
+ ], [] /* no infos */, false /* blue color */);
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP | HAS_OWN_VM_REGIONS,
+ FIELD | DUMP | HAS_OWN_VM_REGIONS,
+ 0,
+ FIELD | DUMP | HAS_OWN_VM_REGIONS
+ ], [] /* no infos */, false /* blue color */);
+
+ // No process memory dumps have own VM regions.
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP
+ ], [EXPECTED_ALL_OLDER_VALUES], true /* light blue */);
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP,
+ FIELD | DUMP
+ ], [EXPECTED_ALL_OLDER_VALUES], true /* light blue */);
+
+ // Some process memory dumps don't have own VM regions.
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP,
+ 0,
+ FIELD | DUMP
+ ], [EXPECTED_SOME_OLDER_VALUES], true /* light blue */);
+ checkByteStatColumnInfosAndColor([
+ FIELD | DUMP | HAS_OWN_VM_REGIONS,
+ FIELD | DUMP,
+ FIELD | DUMP | HAS_OWN_VM_REGIONS
+ ], [EXPECTED_SOME_OLDER_VALUES], false /* blue */);
+ });
+
+ test('allocatorColumn', function() {
+ const c = new AllocatorColumn('Allocator', 'bytes', (x => x),
+ AggregationMode.MAX);
+ checkColorLegend(c.title, 'Allocator');
+ checkColor(c.color(undefined /* contexts */),
+ undefined /* no color (column cells) */);
+
+ const HAS_HEAP_DUMPS = 1 << 2;
+ const HAS_ALLOCATOR_HEAP_DUMP = 1 << 3;
+ const MISSING_SIZE = 1 << 4;
+ function checkAllocatorColumnInfosAndColor(fieldAndDumpMask,
+ expectedInfos) {
+ checkOverviewColumnInfosAndColor(c,
+ fieldAndDumpMask,
+ function(pmd, mask) {
+ if (mask & HAS_HEAP_DUMPS) {
+ pmd.heapDumps = {};
+ }
+ if (mask & HAS_ALLOCATOR_HEAP_DUMP) {
+ pmd.heapDumps.Allocator = new HeapDump(pmd, 'Allocator');
+ }
+ const mad = new MemoryAllocatorDump(pmd, 'Allocator');
+ if (!(mask & MISSING_SIZE)) {
+ mad.addNumeric('size',
+ new Scalar(sizeInBytes_smallerIsBetter, 7));
+ }
+ pmd.memoryAllocatorDumps = [mad];
+ },
+ expectedInfos,
+ undefined /* no color */);
+ }
+
+ // No context.
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+ checkOverviewColumnInfosAndColor(c,
+ [FIELD, FIELD, 0, FIELD],
+ undefined /* no context */,
+ [] /* no infos */,
+ undefined /* no color */);
+
+ // No infos.
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP
+ ], [] /* no infos */);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP,
+ FIELD | DUMP | HAS_HEAP_DUMPS,
+ 0,
+ FIELD | DUMP
+ ], [] /* infos */);
+
+ const EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP = {
+ icon: '\u2630',
+ message: 'Heap dump provided.'
+ };
+ const EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP = {
+ icon: '\u2630',
+ message: 'Heap dump provided at some selected timestamps.'
+ };
+
+ // All process memory dumps have heap dumps.
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP
+ ], [EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP,
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP,
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP
+ ], [EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP]);
+
+ // Some process memory dumps have heap dumps.
+ checkAllocatorColumnInfosAndColor([
+ 0,
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP
+ ], [EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP,
+ FIELD | DUMP | HAS_HEAP_DUMPS,
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP
+ ], [EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP]);
+
+ const EXPECTED_ALL_MISSING_SIZE = {
+ icon: '\u26A0',
+ message: 'Size was not provided.',
+ color: 'red'
+ };
+ const EXPECTED_SOME_MISSING_SIZE = {
+ icon: '\u26A0',
+ message: 'Size was not provided at some selected timestamps.',
+ color: 'red'
+ };
+
+ // All process memory dumps are missing allocator size.
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | MISSING_SIZE
+ ], [EXPECTED_ALL_MISSING_SIZE]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | MISSING_SIZE,
+ FIELD | DUMP | MISSING_SIZE,
+ FIELD | DUMP | MISSING_SIZE
+ ], [EXPECTED_ALL_MISSING_SIZE]);
+
+ // Some process memory dumps use Android memtrack PSS fallback.
+ checkAllocatorColumnInfosAndColor([
+ 0,
+ FIELD | DUMP | MISSING_SIZE
+ ], [EXPECTED_SOME_MISSING_SIZE]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | MISSING_SIZE,
+ FIELD | DUMP,
+ FIELD | DUMP | MISSING_SIZE
+ ], [EXPECTED_SOME_MISSING_SIZE]);
+
+ // Combination of heap dump and memtrack fallback infos.
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | MISSING_SIZE | HAS_HEAP_DUMPS |
+ HAS_ALLOCATOR_HEAP_DUMP
+ ], [
+ EXPECTED_ALL_HAVE_ALLOCATOR_HEAP_DUMP,
+ EXPECTED_ALL_MISSING_SIZE
+ ]);
+ checkAllocatorColumnInfosAndColor([
+ FIELD | DUMP | HAS_HEAP_DUMPS | HAS_ALLOCATOR_HEAP_DUMP,
+ FIELD | DUMP,
+ FIELD | DUMP | MISSING_SIZE
+ ], [
+ EXPECTED_SOME_HAVE_ALLOCATOR_HEAP_DUMP,
+ EXPECTED_SOME_MISSING_SIZE
+ ]);
+ });
+
+ test('tracingColumn', function() {
+ const c = new TracingColumn('Tracing', 'bytes', (x => x),
+ AggregationMode.DIFF);
+ checkSpanWithColor(c.title, 'Tracing',
+ TracingColumn.COLOR /* expected column title gray color */);
+ checkColor(c.color(undefined /* contexts */),
+ TracingColumn.COLOR /* expected column cells gray color */);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html
new file mode 100644
index 00000000000..2f702242140
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_test_utils.html
@@ -0,0 +1,593 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color.html">
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/heap_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for memory dump analysis sub-view tests.
+ */
+tr.exportTo('tr.ui.analysis', function() {
+ const Color = tr.b.Color;
+ const ColorScheme = tr.b.ColorScheme;
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const unitlessNumber_smallerIsBetter =
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
+ const HeapDump = tr.model.HeapDump;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+
+ function createMultipleTestGlobalMemoryDumps() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const pA = model.getOrCreateProcess(1);
+ const pB = model.getOrCreateProcess(2);
+ const pC = model.getOrCreateProcess(3);
+ const pD = model.getOrCreateProcess(4);
+
+ // ======================================================================
+ // First timestamp.
+ // ======================================================================
+ const gmd1 = addGlobalMemoryDump(model, {ts: 42});
+
+ // Totals and VM regions.
+ const pmd1A = addProcessMemoryDump(gmd1, pA, {ts: 41});
+ pmd1A.totals = {residentBytes: 31457280 /* 30 MiB */};
+ pmd1A.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ startAddress: 1024,
+ sizeInBytes: 20971520, /* 20 MiB */
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack]',
+ byteStats: {
+ privateDirtyResident: 8388608, /* 8 MiB */
+ sharedCleanResident: 12582912, /* 12 MiB */
+ proportionalResident: 10485760 /* 10 MiB */
+ }
+ })
+ ]);
+
+ // Everything.
+ const pmd1B = addProcessMemoryDump(gmd1, pB, {ts: 42});
+ pmd1B.totals = {
+ residentBytes: 20971520, /* 20 MiB */
+ peakResidentBytes: 41943040, /* 40 MiB */
+ arePeakResidentBytesResettable: false,
+ privateFootprintBytes: 15728640, /* 15 MiB */
+ platformSpecific: {
+ private_bytes: 10485760 /* 10 MiB */
+ }
+ };
+ pmd1B.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ startAddress: 256,
+ sizeInBytes: 6000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ mappedFile: '[stack:20310]',
+ byteStats: {
+ proportionalResident: 15728640, /* 15 MiB */
+ privateDirtyResident: 1572864, /* 1.5 MiB */
+ swapped: 32 /* 32 B */
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 100000,
+ sizeInBytes: 4096,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '/usr/lib/libwtf.so',
+ byteStats: {
+ proportionalResident: 4194304, /* 4 MiB */
+ privateDirtyResident: 0,
+ swapped: 0 /* 32 B */
+ }
+ })
+ ]);
+ pmd1B.memoryAllocatorDumps = [
+ newAllocatorDump(pmd1B, 'malloc',
+ {numerics: {size: 3145728 /* 3 MiB */}}),
+ newAllocatorDump(pmd1B, 'v8', {numerics: {size: 5242880 /* 5 MiB */}}),
+ newAllocatorDump(pmd1B, 'tracing', {numerics: {
+ size: 1048576 /* 1 MiB */,
+ resident_size: 1572864 /* 1.5 MiB */
+ }})
+ ];
+
+ // Allocator dumps only.
+ const pmd1C = addProcessMemoryDump(gmd1, pC, {ts: 43});
+ pmd1C.memoryAllocatorDumps = (function() {
+ const oilpanDump = newAllocatorDump(pmd1C, 'oilpan', {numerics: {
+ size: 3221225472 /* 3 GiB */,
+ inner_size: 5242880 /* 5 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 2015)
+ }});
+ const v8Dump = newAllocatorDump(pmd1C, 'v8', {numerics: {
+ size: 1073741824 /* 1 GiB */,
+ inner_size: 2097152 /* 2 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204)
+ }});
+
+ addOwnershipLink(v8Dump, oilpanDump);
+
+ return [oilpanDump, v8Dump];
+ })();
+ pmd1C.heapDumps = {
+ 'v8': (function() {
+ const v8HeapDump = new HeapDump(pmd1C, 'v8');
+ v8HeapDump.addEntry(
+ tr.c.TestUtils.newStackTrace(model,
+ ['V8.Execute', 'UpdateLayoutTree']),
+ undefined /* sum over all object types */,
+ 536870912 /* 512 MiB */);
+ return v8HeapDump;
+ })()
+ };
+
+ // ======================================================================
+ // Second timestamp.
+ // ======================================================================
+ const gmd2 = addGlobalMemoryDump(model, {ts: 68});
+
+ // Everything.
+ const pmd2A = addProcessMemoryDump(gmd2, pA, {ts: 67});
+ pmd2A.totals = {residentBytes: 32505856 /* 31 MiB */};
+ pmd2A.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ startAddress: 1024,
+ sizeInBytes: 20971520, /* 20 MiB */
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack]',
+ byteStats: {
+ privateDirtyResident: 8388608, /* 8 MiB */
+ sharedCleanResident: 11534336, /* 11 MiB */
+ proportionalResident: 11534336 /* 11 MiB */
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 104857600,
+ sizeInBytes: 5242880, /* 5 MiB */
+ protectionFlags: VMRegion.PROTECTION_FLAG_EXECUTE,
+ mappedFile: '/usr/bin/google-chrome',
+ byteStats: {
+ privateDirtyResident: 0,
+ sharedCleanResident: 4194304, /* 4 MiB */
+ proportionalResident: 524288 /* 512 KiB */
+ }
+ })
+ ]);
+ pmd2A.memoryAllocatorDumps = [
+ newAllocatorDump(pmd2A, 'malloc', {numerics: {
+ size: 9437184 /* 9 MiB */
+ }}),
+ newAllocatorDump(pmd2A, 'tracing', {numerics: {
+ size: 2097152 /* 2 MiB */,
+ resident_size: 2621440 /* 2.5 MiB */
+ }})
+ ];
+
+ // Totals and allocator dumps only.
+ const pmd2B = addProcessMemoryDump(gmd2, pB, {ts: 69});
+ pmd2B.totals = {
+ residentBytes: 19922944, /* 19 MiB */
+ peakResidentBytes: 41943040, /* 40 MiB */
+ arePeakResidentBytesResettable: false,
+ privateFootprintBytes: 15728640, /* 15 MiB */
+ platformSpecific: {
+ private_bytes: 8912896 /* 8.5 MiB */
+ }
+ };
+ pmd2B.memoryAllocatorDumps = [
+ newAllocatorDump(pmd2B, 'malloc', {numerics: {
+ size: 2621440 /* 2.5 MiB */
+ }}),
+ newAllocatorDump(pmd2B, 'v8', {numerics: {
+ size: 5242880 /* 5 MiB */
+ }}),
+ newAllocatorDump(pmd2B, 'blink', {numerics: {
+ size: 7340032 /* 7 MiB */
+ }}),
+ newAllocatorDump(pmd2B, 'oilpan', {numerics: {size: 1}}),
+ newAllocatorDump(pmd2B, 'tracing', {numerics: {
+ size: 1572864 /* 1.5 MiB */,
+ resident_size: 2097152 /* 2 MiB */
+ }}),
+ newAllocatorDump(pmd2B, 'gpu', {numerics: {
+ memtrack_pss: 524288 /* 512 KiB */
+ }})
+ ];
+
+ // Resettable peak total size only.
+ const pmd2D = addProcessMemoryDump(gmd2, pD, {ts: 71});
+ pmd2D.totals = {
+ peakResidentBytes: 17825792, /* 17 MiB */
+ arePeakResidentBytesResettable: true
+ };
+
+ // ======================================================================
+ // Third timestamp.
+ // ======================================================================
+ const gmd3 = addGlobalMemoryDump(model, {ts: 100});
+
+ // Everything.
+ const pmd3B = addProcessMemoryDump(gmd3, pB, {ts: 102});
+ pmd3B.totals = {
+ residentBytes: 18874368, /* 18 MiB */
+ peakResidentBytes: 44040192, /* 42 MiB */
+ privateFootprintBytes: 15728640, /* 16 MiB */
+ arePeakResidentBytesResettable: false,
+ platformSpecific: {
+ private_bytes: 7340032 /* 7 MiB */
+ }
+ };
+ pmd3B.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ startAddress: 256,
+ sizeInBytes: 6000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ mappedFile: '[stack:20310]',
+ byteStats: {
+ proportionalResident: 21495808, /* 20.5 MiB */
+ privateDirtyResident: 524288, /* 0.5 MiB */
+ swapped: 64 /* 32 B */
+ }
+ })
+ ]);
+ pmd3B.memoryAllocatorDumps = [
+ newAllocatorDump(pmd3B, 'malloc', {numerics: {
+ size: 2883584 /* 2.75 MiB */
+ }}),
+ newAllocatorDump(pmd3B, 'v8', {numerics: {
+ size: 5767168 /* 5.5 MiB */
+ }}),
+ newAllocatorDump(pmd3B, 'blink', {numerics: {
+ size: 6291456 /* 7 MiB */
+ }}),
+ newAllocatorDump(pmd3B, 'tracing', {numerics: {
+ size: 2097152 /* 2 MiB */,
+ resident_size: 3145728 /* 3 MiB */
+ }}),
+ newAllocatorDump(pmd3B, 'gpu', {numerics: {
+ size: 1048576 /* 1 MiB */,
+ memtrack_pss: 786432 /* 768 KiB */
+ }})
+ ];
+
+ // Allocator dumps only.
+ const pmd3C = addProcessMemoryDump(gmd3, pC, {ts: 100});
+ pmd3C.memoryAllocatorDumps = (function() {
+ const oilpanDump = newAllocatorDump(pmd3C, 'oilpan', {numerics: {
+ size: 3221225472 /* 3 GiB */,
+ inner_size: 5242880 /* 5 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 2015)
+ }});
+ const v8Dump = newAllocatorDump(pmd3C, 'v8', {numerics: {
+ size: 2147483648 /* 2 GiB */,
+ inner_size: 2097152 /* 2 MiB */,
+ objects_count: new Scalar(unitlessNumber_smallerIsBetter, 204)
+ }});
+
+ addOwnershipLink(v8Dump, oilpanDump);
+
+ return [oilpanDump, v8Dump];
+ })();
+ pmd3C.heapDumps = {
+ 'v8': (function() {
+ const v8HeapDump = new HeapDump(pmd1C, 'v8');
+ v8HeapDump.addEntry(
+ tr.c.TestUtils.newStackTrace(model,
+ ['V8.Execute', 'UpdateLayoutTree']),
+ undefined /* sum over all object types */,
+ 268435456 /* 256 MiB */);
+ v8HeapDump.addEntry(
+ tr.c.TestUtils.newStackTrace(model,
+ ['V8.Execute', 'FrameView::layout']),
+ undefined /* sum over all object types */,
+ 134217728 /* 128 MiB */);
+ return v8HeapDump;
+ })()
+ };
+
+ // Resettable peak total size only.
+ const pmd3D = addProcessMemoryDump(gmd3, pD, {ts: 99});
+ pmd3D.totals = {
+ peakResidentBytes: 17825792, /* 17 MiB */
+ arePeakResidentBytesResettable: true
+ };
+ });
+
+ return model.globalMemoryDumps;
+ }
+
+ function createSingleTestGlobalMemoryDump() {
+ return createMultipleTestGlobalMemoryDumps()[1];
+ }
+
+ function createMultipleTestProcessMemoryDumps() {
+ return createMultipleTestGlobalMemoryDumps().map(function(gmd) {
+ return gmd.processMemoryDumps[2];
+ });
+ }
+
+ function createSingleTestProcessMemoryDump() {
+ return createMultipleTestProcessMemoryDumps()[1];
+ }
+
+ function checkNumericFields(row, column, expectedValues, expectedUnit) {
+ let fields;
+ if (column === undefined) {
+ fields = row;
+ } else {
+ fields = column.fields(row);
+ }
+
+ if (expectedValues === undefined) {
+ assert.isUndefined(fields);
+ return;
+ }
+
+ assert.lengthOf(fields, expectedValues.length);
+ for (let i = 0; i < fields.length; i++) {
+ const field = fields[i];
+ const expectedValue = expectedValues[i];
+ if (expectedValue === undefined) {
+ assert.isUndefined(field);
+ } else {
+ assert.isDefined(expectedUnit); // Test sanity check.
+ assert.instanceOf(field, Scalar);
+ assert.strictEqual(field.value, expectedValue);
+ assert.strictEqual(field.unit, expectedUnit);
+ }
+ }
+ }
+
+ function checkSizeNumericFields(row, column, expectedValues) {
+ checkNumericFields(row, column, expectedValues,
+ sizeInBytes_smallerIsBetter);
+ }
+
+ function checkStringFields(row, column, expectedStrings) {
+ const fields = column.fields(row);
+
+ if (expectedStrings === undefined) {
+ assert.isUndefined(fields);
+ return;
+ }
+
+ assert.deepEqual(Array.from(fields), expectedStrings);
+ }
+
+ /**
+ * Check the titles, types and aggregation modes of a list of columns.
+ * expectedColumns is a list of dictionaries with the following fields:
+ *
+ * - title: Either the expected title (string), or a matcher for it
+ * (function that accepts the actual title as its argument).
+ * - type: The expected class of the column.
+ * - noAggregation: If true, the column is expected to have no aggregation
+ * mode (regardless of expectedAggregationMode).
+ */
+ function checkColumns(columns, expectedColumns, expectedAggregationMode) {
+ assert.lengthOf(columns, expectedColumns.length);
+ for (let i = 0; i < expectedColumns.length; i++) {
+ const actualColumn = columns[i];
+ const expectedColumn = expectedColumns[i];
+ const expectedTitle = expectedColumn.title;
+ if (typeof expectedTitle === 'function') {
+ expectedTitle(actualColumn.title); // Custom title matcher.
+ } else if (actualColumn.title.innerText) {
+ // HTML title.
+ assert.strictEqual(actualColumn.title.innerText, expectedTitle);
+ } else {
+ assert.strictEqual(actualColumn.title, expectedTitle); // String title.
+ }
+ assert.instanceOf(actualColumn, expectedColumn.type);
+ assert.strictEqual(actualColumn.aggregationMode,
+ expectedColumn.noAggregation ? undefined : expectedAggregationMode);
+ }
+ }
+
+ function checkColumnInfosAndColor(
+ column, fields, contexts, expectedInfos, expectedColorReservedName) {
+ // Test sanity checks.
+ assert.isDefined(fields);
+ if (contexts !== undefined) {
+ assert.lengthOf(contexts, fields.length);
+ }
+
+ // Check infos.
+ const infos = [];
+ column.addInfos(fields, contexts, infos);
+ assert.lengthOf(infos, expectedInfos.length);
+ for (let i = 0; i < expectedInfos.length; i++) {
+ assert.deepEqual(infos[i], expectedInfos[i]);
+ }
+
+ // Check color.
+ const actualColor = typeof column.color === 'function' ?
+ column.color(fields, contexts) :
+ column.color;
+ checkColor(actualColor, expectedColorReservedName);
+ }
+
+ function checkColor(actualColorString, expectedColorString) {
+ if (actualColorString === undefined) {
+ assert.isUndefined(expectedColorString);
+ return;
+ }
+ const actualColor = Color.fromString(actualColorString);
+ const expectedColor = Color.fromString(expectedColorString);
+ assert.deepEqual(actualColor, expectedColor);
+ }
+
+ function createAndCheckEmptyPanes(
+ test, paneTagName, propertyName, opt_callback) {
+ // Unset property.
+ const unsetViewEl = createTestPane(paneTagName);
+ unsetViewEl.rebuild();
+ assert.isUndefined(unsetViewEl.createChildPane());
+ test.addHTMLOutput(unsetViewEl);
+
+ // Undefined property.
+ const undefinedViewEl = createTestPane(paneTagName);
+ undefinedViewEl[propertyName] = undefined;
+ undefinedViewEl.rebuild();
+ assert.isUndefined(undefinedViewEl.createChildPane());
+ test.addHTMLOutput(undefinedViewEl);
+
+ // Empty property.
+ const emptyViewEl = createTestPane(paneTagName);
+ emptyViewEl[propertyName] = [];
+ emptyViewEl.rebuild();
+ assert.isUndefined(undefinedViewEl.createChildPane());
+ test.addHTMLOutput(emptyViewEl);
+
+ // Check that all the panes have the same dimensions.
+ const unsetBounds = unsetViewEl.getBoundingClientRect();
+ const undefinedBounds = undefinedViewEl.getBoundingClientRect();
+ const emptyBounds = emptyViewEl.getBoundingClientRect();
+ assert.strictEqual(undefinedBounds.width, unsetBounds.width);
+ assert.strictEqual(emptyBounds.width, unsetBounds.width);
+ assert.strictEqual(undefinedBounds.height, unsetBounds.height);
+ assert.strictEqual(emptyBounds.height, unsetBounds.height);
+
+ // Custom checks (if provided).
+ if (opt_callback) {
+ opt_callback(unsetViewEl);
+ opt_callback(undefinedViewEl);
+ opt_callback(emptyViewEl);
+ }
+ }
+
+ function createTestPane(tagName) {
+ const paneEl = document.createElement(tagName);
+
+ // Store a list of requested child panes (for inspection in tests).
+ paneEl.requestedChildPanes = [];
+ paneEl.addEventListener('request-child-pane-change', function() {
+ paneEl.requestedChildPanes.push(paneEl.createChildPane());
+ });
+
+ paneEl.createChildPane = function() {
+ const childPaneBuilder = this.childPaneBuilder;
+ if (childPaneBuilder === undefined) return undefined;
+ return childPaneBuilder();
+ };
+
+ return paneEl;
+ }
+
+ // TODO(petrcermak): Consider moving this to tracing/ui/base/dom_helpers.html.
+ function isElementDisplayed(element) {
+ const style = getComputedStyle(element);
+ const displayed = style.display;
+ if (displayed === undefined) return true;
+ return displayed.indexOf('none') === -1;
+ }
+
+ /**
+ * Convert a list of ContainerMemoryDump(s) to a list of dictionaries of the
+ * underlying ProcessMemoryDump(s).
+ */
+ function convertToProcessMemoryDumps(containerMemoryDumps) {
+ return containerMemoryDumps.map(function(containerMemoryDump) {
+ return containerMemoryDump.processMemoryDumps;
+ });
+ }
+
+ /**
+ * Extract a chronological list of ProcessMemoryDump(s) (for a given process)
+ * from a chronological list of dictionaries of ProcessMemoryDump(s).
+ */
+ function extractProcessMemoryDumps(processMemoryDumps, pid) {
+ return processMemoryDumps.map(function(memoryDumps) {
+ return memoryDumps[pid];
+ });
+ }
+
+ /**
+ * Extract a chronological list of lists of VMRegion(s) (for a given process)
+ * from a chronological list of dictionaries of ProcessMemoryDump(s).
+ */
+ function extractVmRegions(processMemoryDumps, pid) {
+ return processMemoryDumps.map(function(memoryDumps) {
+ const processMemoryDump = memoryDumps[pid];
+ if (processMemoryDump === undefined) return undefined;
+ return processMemoryDump.mostRecentVmRegions;
+ });
+ }
+
+ /**
+ * Extract a chronological list of MemoryAllocatorDump(s) (for a given
+ * process and allocator name) from a chronological list of dictionaries of
+ * ProcessMemoryDump(s).
+ */
+ function extractMemoryAllocatorDumps(processMemoryDumps, pid, allocatorName) {
+ return processMemoryDumps.map(function(memoryDumps) {
+ const processMemoryDump = memoryDumps[pid];
+ if (processMemoryDump === undefined) return undefined;
+ return processMemoryDump.getMemoryAllocatorDumpByFullName(allocatorName);
+ });
+ }
+
+ /**
+ * Extract a chronological list of HeapDump(s) (for a given process and
+ * allocator name) from a chronological list of dictionaries of
+ * ProcessMemoryDump(s).
+ */
+ function extractHeapDumps(processMemoryDumps, pid, allocatorName) {
+ return processMemoryDumps.map(function(memoryDumps) {
+ const processMemoryDump = memoryDumps[pid];
+ if (processMemoryDump === undefined ||
+ processMemoryDump.heapDumps === undefined) {
+ return undefined;
+ }
+ return processMemoryDump.heapDumps[allocatorName];
+ });
+ }
+
+ return {
+ createSingleTestGlobalMemoryDump,
+ createMultipleTestGlobalMemoryDumps,
+ createSingleTestProcessMemoryDump,
+ createMultipleTestProcessMemoryDumps,
+ checkNumericFields,
+ checkSizeNumericFields,
+ checkStringFields,
+ checkColumns,
+ checkColumnInfosAndColor,
+ checkColor,
+ createAndCheckEmptyPanes,
+ createTestPane,
+ isElementDisplayed,
+ convertToProcessMemoryDumps,
+ extractProcessMemoryDumps,
+ extractVmRegions,
+ extractMemoryAllocatorDumps,
+ extractHeapDumps,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html
new file mode 100644
index 00000000000..654ce0ea73f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util.html
@@ -0,0 +1,915 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper code for memory dump sub-views.
+ */
+tr.exportTo('tr.ui.analysis', function() {
+ const NO_BREAK_SPACE = String.fromCharCode(160);
+ const RIGHTWARDS_ARROW = String.fromCharCode(8594);
+
+ const COLLATOR = new Intl.Collator(undefined, {numeric: true});
+
+ /**
+ * A table column for displaying memory dump row titles.
+ *
+ * @constructor
+ */
+ function TitleColumn(title) {
+ this.title = title;
+ }
+
+ TitleColumn.prototype = {
+ supportsCellSelection: false,
+
+ /**
+ * Get the title associated with a given row.
+ *
+ * This method will decorate the title with color and '+++'/'---' prefix if
+ * appropriate (as determined by the optional row.contexts field).
+ * Examples:
+ *
+ * +----------------------+-----------------+--------+--------+
+ * | Contexts provided at | Interpretation | Prefix | Color |
+ * +----------------------+-----------------+--------+--------+
+ * | 1111111111 | always present | | |
+ * | 0000111111 | added | +++ | red |
+ * | 1111111000 | deleted | --- | green |
+ * | 1100111111* | flaky | | purple |
+ * | 0001001111 | added + flaky | +++ | purple |
+ * | 1111100010 | deleted + flaky | --- | purple |
+ * +----------------------+-----------------+--------+--------+
+ *
+ * *) This means that, given a selection of 10 memory dumps, a particular
+ * row (e.g. a process) was present in the first 2 and last 6 of them
+ * (but not in the third and fourth dump).
+ *
+ * This method should therefore NOT be overriden by subclasses. The
+ * formatTitle method should be overriden instead when necessary.
+ */
+ value(row) {
+ const formattedTitle = this.formatTitle(row);
+
+ const contexts = row.contexts;
+ if (contexts === undefined || contexts.length === 0) {
+ return formattedTitle;
+ }
+
+ // Determine if the row was provided in the first and last row and how
+ // many times it changed between being provided and not provided.
+ const firstContext = contexts[0];
+ const lastContext = contexts[contexts.length - 1];
+ let changeDefinedContextCount = 0;
+ for (let i = 1; i < contexts.length; i++) {
+ if ((contexts[i] === undefined) !== (contexts[i - 1] === undefined)) {
+ changeDefinedContextCount++;
+ }
+ }
+
+ // Determine the color and prefix of the title.
+ let color = undefined;
+ let prefix = undefined;
+ if (!firstContext && lastContext) {
+ // The row was added.
+ color = 'red';
+ prefix = '+++';
+ } else if (firstContext && !lastContext) {
+ // The row was removed.
+ color = 'green';
+ prefix = '---';
+ }
+ if (changeDefinedContextCount > 1) {
+ // The row was flaky (added/removed more than once).
+ color = 'purple';
+ }
+
+ if (color === undefined && prefix === undefined) {
+ return formattedTitle;
+ }
+
+ const titleEl = document.createElement('span');
+ if (prefix !== undefined) {
+ const prefixEl = tr.ui.b.createSpan({textContent: prefix});
+ // Enforce same width of '+++' and '---'.
+ prefixEl.style.fontFamily = 'monospace';
+ Polymer.dom(titleEl).appendChild(prefixEl);
+ Polymer.dom(titleEl).appendChild(
+ tr.ui.b.asHTMLOrTextNode(NO_BREAK_SPACE));
+ }
+ if (color !== undefined) {
+ titleEl.style.color = color;
+ }
+ Polymer.dom(titleEl).appendChild(
+ tr.ui.b.asHTMLOrTextNode(formattedTitle));
+ return titleEl;
+ },
+
+ /**
+ * Format the title associated with a given row. This method is intended to
+ * be overriden by subclasses.
+ */
+ formatTitle(row) {
+ return row.title;
+ },
+
+ cmp(rowA, rowB) {
+ return COLLATOR.compare(rowA.title, rowB.title);
+ }
+ };
+
+ /**
+ * Abstract table column for displaying memory dump data.
+ *
+ * @constructor
+ */
+ function MemoryColumn(name, cellPath, aggregationMode) {
+ this.name = name;
+ this.cellPath = cellPath;
+ this.shouldSetContextGroup = false;
+
+ // See MemoryColumn.AggregationMode enum in this file.
+ this.aggregationMode = aggregationMode;
+ }
+
+ /**
+ * Construct columns from cells in a hierarchy of rows and a list of rules.
+ *
+ * The list of rules contains objects with three fields:
+ *
+ * condition: Optional string or regular expression matched against the
+ * name of a cell. If omitted, the rule will match any cell.
+ * importance: Mandatory number which determines the final order of the
+ * columns. The column with the highest importance will be first in the
+ * returned array.
+ * columnConstructor: Mandatory memory column constructor.
+ *
+ * Example:
+ *
+ * const importanceRules = [
+ * {
+ * condition: 'page_size',
+ * columnConstructor: NumericMemoryColumn,
+ * importance: 8
+ * },
+ * {
+ * condition: /size/,
+ * columnConstructor: CustomNumericMemoryColumn,
+ * importance: 10
+ * },
+ * {
+ * // No condition: matches all columns.
+ * columnConstructor: NumericMemoryColumn,
+ * importance: 9
+ * }
+ * ];
+ *
+ * Given a name of a cell, the corresponding column constructor and
+ * importance are determined by the first rule whose condition matches the
+ * column's name. For example, given a cell with name 'inner_size', the
+ * corresponding column will be constructed using CustomNumericMemoryColumn
+ * and its importance (for sorting purposes) will be 10 (second rule).
+ *
+ * After columns are constructed for all cell names, they are sorted in
+ * descending order of importance and the resulting list is returned. In the
+ * example above, the constructed columns will be sorted into three groups as
+ * follows:
+ *
+ * [most important, left in the resulting table]
+ * 1. columns whose name contains 'size' excluding 'page_size' because it
+ * would have already matched the first rule (Note that string matches
+ * must be exact so a column named 'page_size2' would not match the
+ * first rule and would therefore belong to this group).
+ * 2. columns whose name does not contain 'size'.
+ * 3. columns whose name is 'page_size'.
+ * [least important, right in the resulting table]
+ *
+ * where columns will be sorted alphabetically within each group.
+ *
+ * @param {!Array.<!Object>} rows
+ * @param {!Object} config
+ * @param {string} config.cellKey
+ * @param {!MemoryColumn.AggregationMode=} config.aggregationMode
+ * @param {!Array.<!{
+ * condition: (string|!RegExp)=,
+ * importance: number,
+ * columnConstructor: !function(new: MemoryColumn, ...)=,
+ * shouldSetContextGroup: boolean=
+ * }>} config.rules
+ */
+ MemoryColumn.fromRows = function(rows, config) {
+ // Recursively find the names of all cells of the rows (and their sub-rows).
+ const cellNames = new Set();
+ function gatherCellNames(rows) {
+ rows.forEach(function(row) {
+ if (row === undefined) return;
+ const fieldCells = row[config.cellKey];
+ if (fieldCells !== undefined) {
+ for (const [fieldName, fieldCell] of Object.entries(fieldCells)) {
+ if (fieldCell === undefined || fieldCell.fields === undefined) {
+ continue;
+ }
+ cellNames.add(fieldName);
+ }
+ }
+ const subRows = row.subRows;
+ if (subRows !== undefined) {
+ gatherCellNames(subRows);
+ }
+ });
+ }
+ gatherCellNames(rows);
+
+ // Based on the provided list of rules, construct the columns and calculate
+ // their importance.
+ const positions = [];
+ cellNames.forEach(function(cellName) {
+ const cellPath = [config.cellKey, cellName];
+ const matchingRule = MemoryColumn.findMatchingRule(
+ cellName, config.rules);
+ const constructor = matchingRule.columnConstructor;
+ const column = new constructor(
+ cellName, cellPath, config.aggregationMode);
+ column.shouldSetContextGroup = !!config.shouldSetContextGroup;
+ positions.push({
+ importance: matchingRule.importance,
+ column
+ });
+ });
+
+ positions.sort(function(a, b) {
+ // Sort columns with the same importance alphabetically.
+ if (a.importance === b.importance) {
+ return COLLATOR.compare(a.column.name, b.column.name);
+ }
+
+ // Sort columns in descending order of importance.
+ return b.importance - a.importance;
+ });
+
+ return positions.map(function(position) { return position.column; });
+ };
+
+ MemoryColumn.spaceEqually = function(columns) {
+ const columnWidth = (100 / columns.length).toFixed(3) + '%';
+ columns.forEach(function(column) {
+ column.width = columnWidth;
+ });
+ };
+
+ MemoryColumn.findMatchingRule = function(name, rules) {
+ for (let i = 0; i < rules.length; i++) {
+ const rule = rules[i];
+ if (MemoryColumn.nameMatchesCondition(name, rule.condition)) {
+ return rule;
+ }
+ }
+ return undefined;
+ };
+
+ MemoryColumn.nameMatchesCondition = function(name, condition) {
+ // Rules without conditions match all columns.
+ if (condition === undefined) return true;
+
+ // String conditions must match the column name exactly.
+ if (typeof(condition) === 'string') return name === condition;
+
+ // If the condition is not a string, assume it is a RegExp.
+ return condition.test(name);
+ };
+
+ /** @enum */
+ MemoryColumn.AggregationMode = {
+ DIFF: 0,
+ MAX: 1
+ };
+
+ MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER = 'at some selected timestamps';
+
+ MemoryColumn.prototype = {
+ get title() {
+ return this.name;
+ },
+
+ cell(row) {
+ let cell = row;
+ const cellPath = this.cellPath;
+ for (let i = 0; i < cellPath.length; i++) {
+ if (cell === undefined) return undefined;
+ cell = cell[cellPath[i]];
+ }
+ return cell;
+ },
+
+ aggregateCells(row, subRows) {
+ // No generic aggregation.
+ },
+
+ fields(row) {
+ const cell = this.cell(row);
+ if (cell === undefined) return undefined;
+ return cell.fields;
+ },
+
+ /**
+ * Format a cell associated with this column from the given row. This
+ * method is not intended to be overriden.
+ */
+ value(row) {
+ const fields = this.fields(row);
+ if (this.hasAllRelevantFieldsUndefined(fields)) return '';
+
+ // Determine the color and infos of the resulting element.
+ const contexts = row.contexts;
+ const color = this.color(fields, contexts);
+ const infos = [];
+ this.addInfos(fields, contexts, infos);
+
+ // Format the actual fields.
+ const formattedFields = this.formatFields(fields);
+
+ // If no color is specified and there are no infos, there is no need to
+ // wrap the value in a span element.#
+ if ((color === undefined || formattedFields === '') &&
+ infos.length === 0) {
+ return formattedFields;
+ }
+
+ const fieldEl = document.createElement('span');
+ fieldEl.style.display = 'flex';
+ fieldEl.style.alignItems = 'center';
+ fieldEl.style.justifyContent = 'flex-end';
+ Polymer.dom(fieldEl).appendChild(
+ tr.ui.b.asHTMLOrTextNode(formattedFields));
+
+ // Add info icons with tooltips.
+ infos.forEach(function(info) {
+ const infoEl = document.createElement('span');
+ infoEl.style.paddingLeft = '4px';
+ infoEl.style.cursor = 'help';
+ infoEl.style.fontWeight = 'bold';
+ Polymer.dom(infoEl).textContent = info.icon;
+ if (info.color !== undefined) {
+ infoEl.style.color = info.color;
+ }
+ infoEl.title = info.message;
+ Polymer.dom(fieldEl).appendChild(infoEl);
+ }, this);
+
+ // Set the color of the element.
+ if (color !== undefined) {
+ fieldEl.style.color = color;
+ }
+
+ return fieldEl;
+ },
+
+ /**
+ * Returns true iff all fields of a row which are relevant for the current
+ * aggregation mode (e.g. first and last field for diff mode) are undefined.
+ */
+ hasAllRelevantFieldsUndefined(fields) {
+ if (fields === undefined) return true;
+
+ switch (this.aggregationMode) {
+ case MemoryColumn.AggregationMode.DIFF:
+ // Only the first and last field are relevant.
+ return fields[0] === undefined &&
+ fields[fields.length - 1] === undefined;
+
+ case MemoryColumn.AggregationMode.MAX:
+ default:
+ // All fields are relevant.
+ return fields.every(function(field) { return field === undefined; });
+ }
+ },
+
+ /**
+ * Get the color of the given fields formatted by this column. At least one
+ * field relevant for the current aggregation mode is guaranteed to be
+ * defined.
+ */
+ color(fields, contexts) {
+ return undefined;
+ },
+
+ /**
+ * Format an arbitrary number of fields. At least one field relevant for
+ * the current aggregation mode is guaranteed to be defined.
+ */
+ formatFields(fields) {
+ if (fields.length === 1) {
+ return this.formatSingleField(fields[0]);
+ }
+ return this.formatMultipleFields(fields);
+ },
+
+ /**
+ * Format a single defined field.
+ *
+ * This method is intended to be overriden by field type specific columns
+ * (e.g. show '1.0 KiB' instead of '1024' for Scalar(s) representing
+ * bytes).
+ */
+ formatSingleField(field) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Format multiple fields. At least one field relevant for the current
+ * aggregation mode is guaranteed to be defined.
+ *
+ * The aggregation mode specializations of this method (e.g.
+ * formatMultipleFieldsDiff) are intended to be overriden by field type
+ * specific columns.
+ */
+ formatMultipleFields(fields) {
+ switch (this.aggregationMode) {
+ case MemoryColumn.AggregationMode.DIFF:
+ return this.formatMultipleFieldsDiff(
+ fields[0], fields[fields.length - 1]);
+
+ case MemoryColumn.AggregationMode.MAX:
+ return this.formatMultipleFieldsMax(fields);
+
+ default:
+ return tr.ui.b.createSpan({
+ textContent: '(unsupported aggregation mode)',
+ italic: true
+ });
+ }
+ },
+
+ formatMultipleFieldsDiff(firstField, lastField) {
+ throw new Error('Not implemented');
+ },
+
+ formatMultipleFieldsMax(fields) {
+ return this.formatSingleField(this.getMaxField(fields));
+ },
+
+ cmp(rowA, rowB) {
+ const fieldsA = this.fields(rowA);
+ const fieldsB = this.fields(rowB);
+
+ // Sanity check.
+ if (fieldsA !== undefined && fieldsB !== undefined &&
+ fieldsA.length !== fieldsB.length) {
+ throw new Error('Different number of fields');
+ }
+
+ // Handle empty fields.
+ const undefinedA = this.hasAllRelevantFieldsUndefined(fieldsA);
+ const undefinedB = this.hasAllRelevantFieldsUndefined(fieldsB);
+ if (undefinedA && undefinedB) return 0;
+ if (undefinedA) return -1;
+ if (undefinedB) return 1;
+
+ return this.compareFields(fieldsA, fieldsB);
+ },
+
+ /**
+ * Compare a pair of single or multiple fields. At least one field relevant
+ * for the current aggregation mode is guaranteed to be defined in each of
+ * the two lists.
+ */
+ compareFields(fieldsA, fieldsB) {
+ if (fieldsA.length === 1) {
+ return this.compareSingleFields(fieldsA[0], fieldsB[0]);
+ }
+ return this.compareMultipleFields(fieldsA, fieldsB);
+ },
+
+ /**
+ * Compare a pair of single defined fields.
+ *
+ * This method is intended to be overriden by field type specific columns.
+ */
+ compareSingleFields(fieldA, fieldB) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Compare a pair of multiple fields. At least one field relevant for the
+ * current aggregation mode is guaranteed to be defined in each of the two
+ * lists.
+ *
+ * The aggregation mode specializations of this method (e.g.
+ * compareMultipleFieldsDiff) are intended to be overriden by field type
+ * specific columns.
+ */
+ compareMultipleFields(fieldsA, fieldsB) {
+ switch (this.aggregationMode) {
+ case MemoryColumn.AggregationMode.DIFF:
+ return this.compareMultipleFieldsDiff(
+ fieldsA[0], fieldsA[fieldsA.length - 1],
+ fieldsB[0], fieldsB[fieldsB.length - 1]);
+
+ case MemoryColumn.AggregationMode.MAX:
+ return this.compareMultipleFieldsMax(fieldsA, fieldsB);
+
+ default:
+ return 0;
+ }
+ },
+
+ compareMultipleFieldsDiff(firstFieldA, lastFieldA, firstFieldB,
+ lastFieldB) {
+ throw new Error('Not implemented');
+ },
+
+ compareMultipleFieldsMax(fieldsA, fieldsB) {
+ return this.compareSingleFields(
+ this.getMaxField(fieldsA), this.getMaxField(fieldsB));
+ },
+
+ getMaxField(fields) {
+ return fields.reduce(function(accumulator, field) {
+ if (field === undefined) {
+ return accumulator;
+ }
+ if (accumulator === undefined ||
+ this.compareSingleFields(field, accumulator) > 0) {
+ return field;
+ }
+ return accumulator;
+ }.bind(this), undefined);
+ },
+
+ addInfos(fields, contexts, infos) {
+ // No generic infos.
+ },
+
+ getImportance(importanceRules) {
+ if (importanceRules.length === 0) return 0;
+
+ // Find the first matching rule.
+ const matchingRule =
+ MemoryColumn.findMatchingRule(this.name, importanceRules);
+ if (matchingRule !== undefined) {
+ return matchingRule.importance;
+ }
+
+ // No matching rule. Return lower importance than all rules.
+ let minImportance = importanceRules[0].importance;
+ for (let i = 1; i < importanceRules.length; i++) {
+ minImportance = Math.min(minImportance, importanceRules[i].importance);
+ }
+ return minImportance - 1;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function StringMemoryColumn(name, cellPath, aggregationMode) {
+ MemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ StringMemoryColumn.prototype = {
+ __proto__: MemoryColumn.prototype,
+
+ formatSingleField(string) {
+ return string;
+ },
+
+ formatMultipleFieldsDiff(firstString, lastString) {
+ if (firstString === undefined) {
+ // String was added ("+NEW_VALUE" in red).
+ const spanEl = tr.ui.b.createSpan({color: 'red'});
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode('+'));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ this.formatSingleField(lastString)));
+ return spanEl;
+ } else if (lastString === undefined) {
+ // String was removed ("-OLD_VALUE" in green).
+ const spanEl = tr.ui.b.createSpan({color: 'green'});
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode('-'));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ this.formatSingleField(firstString)));
+ return spanEl;
+ } else if (firstString === lastString) {
+ // String didn't change ("VALUE" with unchanged color).
+ return this.formatSingleField(firstString);
+ }
+ // String changed ("OLD_VALUE -> NEW_VALUE" in orange).
+ const spanEl = tr.ui.b.createSpan({color: 'DarkOrange'});
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ this.formatSingleField(firstString)));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ ' ' + RIGHTWARDS_ARROW + ' '));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode(
+ this.formatSingleField(lastString)));
+ return spanEl;
+ },
+
+ compareSingleFields(stringA, stringB) {
+ return COLLATOR.compare(stringA, stringB);
+ },
+
+ compareMultipleFieldsDiff(firstStringA, lastStringA, firstStringB,
+ lastStringB) {
+ // If one of the strings was added (and the other one wasn't), mark the
+ // corresponding diff as greater.
+ if (firstStringA === undefined && firstStringB !== undefined) {
+ return 1;
+ }
+ if (firstStringA !== undefined && firstStringB === undefined) {
+ return -1;
+ }
+
+ // If both strings were added, compare the last values (greater last
+ // value implies greater diff).
+ if (firstStringA === undefined && firstStringB === undefined) {
+ return this.compareSingleFields(lastStringA, lastStringB);
+ }
+
+ // If one of the strings was removed (and the other one wasn't), mark the
+ // corresponding diff as lower.
+ if (lastStringA === undefined && lastStringB !== undefined) {
+ return -1;
+ }
+ if (lastStringA !== undefined && lastStringB === undefined) {
+ return 1;
+ }
+
+ // If both strings were removed, compare the first values (greater first
+ // value implies smaller (!) diff).
+ if (lastStringA === undefined && lastStringB === undefined) {
+ return this.compareSingleFields(firstStringB, firstStringA);
+ }
+
+ const areStringsAEqual = firstStringA === lastStringA;
+ const areStringsBEqual = firstStringB === lastStringB;
+
+ // Consider diffs of strings that did not change to be smaller than diffs
+ // of strings that did change.
+ if (areStringsAEqual && areStringsBEqual) return 0;
+ if (areStringsAEqual) return -1;
+ if (areStringsBEqual) return 1;
+
+ // Both strings changed. We are unable to determine the ordering of the
+ // diffs.
+ return 0;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function NumericMemoryColumn(name, cellPath, aggregationMode) {
+ MemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ // Avoid tiny positive/negative diffs (displayed in the UI as '+0.0 B' and
+ // '-0.0 B') due to imprecise floating-point arithmetic by treating all diffs
+ // within the (-DIFF_EPSILON, DIFF_EPSILON) range as zeros.
+ NumericMemoryColumn.DIFF_EPSILON = 0.0001;
+
+ NumericMemoryColumn.prototype = {
+ __proto__: MemoryColumn.prototype,
+
+ align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT,
+
+ aggregateCells(row, subRows) {
+ const subRowCells = subRows.map(this.cell, this);
+
+ // Determine if there is at least one defined numeric in the sub-row
+ // cells and the timestamp count.
+ let hasDefinedSubRowNumeric = false;
+ let timestampCount = undefined;
+ subRowCells.forEach(function(subRowCell) {
+ if (subRowCell === undefined) return;
+
+ const subRowNumerics = subRowCell.fields;
+ if (subRowNumerics === undefined) return;
+
+ if (timestampCount === undefined) {
+ timestampCount = subRowNumerics.length;
+ } else if (timestampCount !== subRowNumerics.length) {
+ throw new Error('Sub-rows have different numbers of timestamps');
+ }
+
+ if (hasDefinedSubRowNumeric) {
+ return; // Avoid unnecessary traversals of the numerics.
+ }
+ hasDefinedSubRowNumeric = subRowNumerics.some(function(numeric) {
+ return numeric !== undefined;
+ });
+ });
+ if (!hasDefinedSubRowNumeric) {
+ return; // No numeric to aggregate.
+ }
+
+ // Get or create the row cell.
+ const cellPath = this.cellPath;
+ let rowCell = row;
+ for (let i = 0; i < cellPath.length; i++) {
+ const nextStepName = cellPath[i];
+ let nextStep = rowCell[nextStepName];
+ if (nextStep === undefined) {
+ if (i < cellPath.length - 1) {
+ nextStep = {};
+ } else {
+ nextStep = new MemoryCell(undefined);
+ }
+ rowCell[nextStepName] = nextStep;
+ }
+ rowCell = nextStep;
+ }
+ if (rowCell.fields === undefined) {
+ rowCell.fields = new Array(timestampCount);
+ } else if (rowCell.fields.length !== timestampCount) {
+ throw new Error(
+ 'Row has a different number of timestamps than sub-rows');
+ }
+
+ for (let i = 0; i < timestampCount; i++) {
+ if (rowCell.fields[i] !== undefined) continue;
+ rowCell.fields[i] = tr.model.MemoryAllocatorDump.aggregateNumerics(
+ subRowCells.map(function(subRowCell) {
+ if (subRowCell === undefined || subRowCell.fields === undefined) {
+ return undefined;
+ }
+ return subRowCell.fields[i];
+ }));
+ }
+ },
+
+ formatSingleField(numeric) {
+ return tr.v.ui.createScalarSpan(numeric, {
+ context: this.getFormattingContext(numeric.unit),
+ contextGroup: this.shouldSetContextGroup ? this.name : undefined,
+ inline: true,
+ });
+ },
+
+ getFormattingContext(unit) {
+ return undefined;
+ },
+
+ formatMultipleFieldsDiff(firstNumeric, lastNumeric) {
+ return this.formatSingleField(
+ this.getDiffField_(firstNumeric, lastNumeric));
+ },
+
+ compareSingleFields(numericA, numericB) {
+ return numericA.value - numericB.value;
+ },
+
+ compareMultipleFieldsDiff(firstNumericA, lastNumericA,
+ firstNumericB, lastNumericB) {
+ return this.getDiffFieldValue_(firstNumericA, lastNumericA) -
+ this.getDiffFieldValue_(firstNumericB, lastNumericB);
+ },
+
+ getDiffField_(firstNumeric, lastNumeric) {
+ const definedNumeric = firstNumeric || lastNumeric;
+ return new tr.b.Scalar(definedNumeric.unit.correspondingDeltaUnit,
+ this.getDiffFieldValue_(firstNumeric, lastNumeric));
+ },
+
+ getDiffFieldValue_(firstNumeric, lastNumeric) {
+ const firstValue = firstNumeric === undefined ? 0 : firstNumeric.value;
+ const lastValue = lastNumeric === undefined ? 0 : lastNumeric.value;
+ const diff = lastValue - firstValue;
+ return Math.abs(diff) < NumericMemoryColumn.DIFF_EPSILON ? 0 : diff;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function MemoryCell(fields) {
+ this.fields = fields;
+ }
+
+ MemoryCell.extractFields = function(cell) {
+ if (cell === undefined) return undefined;
+ return cell.fields;
+ };
+
+ /** Limit for the number of sub-rows for recursive table row expansion. */
+ const RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT = 10;
+
+ function expandTableRowsRecursively(table) {
+ let currentLevelRows = table.tableRows;
+ let totalVisibleRowCount = currentLevelRows.length;
+
+ while (currentLevelRows.length > 0) {
+ // Calculate the total number of sub-rows on the current level.
+ let nextLevelRowCount = 0;
+ currentLevelRows.forEach(function(currentLevelRow) {
+ const subRows = currentLevelRow.subRows;
+ if (subRows === undefined || subRows.length === 0) return;
+ nextLevelRowCount += subRows.length;
+ });
+
+ // Determine whether expanding all rows on the current level would cause
+ // the total number of visible rows go over the limit.
+ if (totalVisibleRowCount + nextLevelRowCount >
+ RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT) {
+ break;
+ }
+
+ // Expand all rows on the current level and gather their sub-rows.
+ const nextLevelRows = new Array(nextLevelRowCount);
+ let nextLevelRowIndex = 0;
+ currentLevelRows.forEach(function(currentLevelRow) {
+ const subRows = currentLevelRow.subRows;
+ if (subRows === undefined || subRows.length === 0) return;
+ table.setExpandedForTableRow(currentLevelRow, true);
+ subRows.forEach(function(subRow) {
+ nextLevelRows[nextLevelRowIndex++] = subRow;
+ });
+ });
+
+ // Update the total number of visible rows and progress to the next level.
+ totalVisibleRowCount += nextLevelRowCount;
+ currentLevelRows = nextLevelRows;
+ }
+ }
+
+ function aggregateTableRowCellsRecursively(row, columns, opt_predicate) {
+ const subRows = row.subRows;
+ if (subRows === undefined || subRows.length === 0) return;
+
+ subRows.forEach(function(subRow) {
+ aggregateTableRowCellsRecursively(subRow, columns, opt_predicate);
+ });
+
+ if (opt_predicate === undefined || opt_predicate(row.contexts)) {
+ aggregateTableRowCells(row, subRows, columns);
+ }
+ }
+
+ function aggregateTableRowCells(row, subRows, columns) {
+ columns.forEach(function(column) {
+ if (!(column instanceof MemoryColumn)) return;
+ column.aggregateCells(row, subRows);
+ });
+ }
+
+ function createCells(timeToValues, valueFieldsGetter, opt_this) {
+ opt_this = opt_this || this;
+ const fieldNameToFields = tr.b.invertArrayOfDicts(
+ timeToValues, valueFieldsGetter, opt_this);
+ const result = {};
+ for (const [fieldName, fields] of Object.entries(fieldNameToFields)) {
+ result[fieldName] = new tr.ui.analysis.MemoryCell(fields);
+ }
+ return result;
+ }
+
+ function createWarningInfo(message) {
+ return {
+ message,
+ icon: String.fromCharCode(9888),
+ color: 'red'
+ };
+ }
+
+ // TODO(petrcermak): Use a context manager instead
+ // (https://github.com/catapult-project/catapult/issues/2420).
+ function DetailsNumericMemoryColumn(name, cellPath, aggregationMode) {
+ NumericMemoryColumn.call(this, name, cellPath, aggregationMode);
+ }
+
+ DetailsNumericMemoryColumn.prototype = {
+ __proto__: NumericMemoryColumn.prototype,
+
+ getFormattingContext(unit) {
+ if (unit.baseUnit === tr.b.Unit.byName.sizeInBytes) {
+ return { unitPrefix: tr.b.UnitPrefixScale.BINARY.KIBI };
+ }
+ return undefined;
+ }
+ };
+
+ return {
+ TitleColumn,
+ MemoryColumn,
+ StringMemoryColumn,
+ NumericMemoryColumn,
+ MemoryCell,
+ expandTableRowsRecursively,
+ aggregateTableRowCellsRecursively,
+ aggregateTableRowCells,
+ createCells,
+ createWarningInfo,
+ DetailsNumericMemoryColumn,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html
new file mode 100644
index 00000000000..859f78433d6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_sub_view_util_test.html
@@ -0,0 +1,1241 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TitleColumn = tr.ui.analysis.TitleColumn;
+ const MemoryColumn = tr.ui.analysis.MemoryColumn;
+ const AggregationMode = MemoryColumn.AggregationMode;
+ const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn;
+ const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn;
+ const MemoryCell = tr.ui.analysis.MemoryCell;
+ const expandTableRowsRecursively = tr.ui.analysis.expandTableRowsRecursively;
+ const aggregateTableRowCells = tr.ui.analysis.aggregateTableRowCells;
+ const aggregateTableRowCellsRecursively =
+ tr.ui.analysis.aggregateTableRowCellsRecursively;
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const checkNumericFields = tr.ui.analysis.checkNumericFields;
+ const checkStringFields = tr.ui.analysis.checkStringFields;
+ const createCells = tr.ui.analysis.createCells;
+ const createWarningInfo = tr.ui.analysis.createWarningInfo;
+
+ function checkPercent(string, expectedPercent) {
+ assert.strictEqual(Number(string.slice(0, -1)), expectedPercent);
+ assert.strictEqual(string.slice(-1), '%');
+ }
+
+ function checkMemoryColumnFieldFormat(test, column, fields,
+ expectedTextContent, opt_expectedColor) {
+ const value = column.formatMultipleFields(fields);
+ if (expectedTextContent === undefined) {
+ assert.strictEqual(value, '');
+ assert.isUndefined(opt_expectedColor); // Test sanity check.
+ return;
+ }
+
+ const node = tr.ui.b.asHTMLOrTextNode(value);
+ const spanEl = document.createElement('span');
+ Polymer.dom(spanEl).appendChild(node);
+ test.addHTMLOutput(spanEl);
+
+ assert.strictEqual(Polymer.dom(node).textContent, expectedTextContent);
+ if (opt_expectedColor === undefined) {
+ assert.notInstanceOf(node, HTMLElement);
+ } else {
+ assert.strictEqual(node.style.color, opt_expectedColor);
+ }
+ }
+
+ function checkCompareFieldsEqual(column, fieldValuesA, fieldValuesB) {
+ assert.strictEqual(column.compareFields(fieldValuesA, fieldValuesB), 0);
+ }
+
+ function checkCompareFieldsLess(column, fieldValuesA, fieldValuesB) {
+ assert.isBelow(column.compareFields(fieldValuesA, fieldValuesB), 0);
+ assert.isAbove(column.compareFields(fieldValuesB, fieldValuesA), 0);
+ }
+
+ function checkNumericMemoryColumnFieldFormat(test, column, fieldValues, unit,
+ expectedValue) {
+ const value = column.formatMultipleFields(
+ buildScalarCell(unit, fieldValues).fields);
+ if (expectedValue === undefined) {
+ assert.strictEqual(value, '');
+ assert.isUndefined(expectedUnits); // Test sanity check.
+ return;
+ }
+
+ test.addHTMLOutput(value);
+ assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(value.value, expectedValue);
+ assert.strictEqual(value.unit, unit);
+ }
+
+ function buildScalarCell(unit, values) {
+ return new MemoryCell(values.map(function(value) {
+ if (value === undefined) return undefined;
+ return new Scalar(unit, value);
+ }));
+ }
+
+ function buildTestRows() {
+ return [
+ {
+ title: 'Row 1',
+ fields: {
+ 'cpu_temperature': new MemoryCell(['below zero', 'absolute zero'])
+ },
+ subRows: [
+ {
+ title: 'Row 1A',
+ fields: {
+ 'page_size': buildScalarCell(sizeInBytes_smallerIsBetter,
+ [1024, 1025])
+ }
+ },
+ {
+ title: 'Row 1B',
+ fields: {
+ 'page_size': buildScalarCell(sizeInBytes_smallerIsBetter,
+ [512, 513]),
+ 'mixed': new MemoryCell(['0.01', '0.10']),
+ 'mixed2': new MemoryCell([
+ new Scalar(tr.b.Unit.byName.powerInWatts, 2.43e18),
+ new Scalar(tr.b.Unit.byName.powerInWatts, 0.5433)
+ ])
+ }
+ }
+ ]
+ },
+ {
+ title: 'Row 2',
+ fields: {
+ 'cpu_temperature': undefined,
+ 'mixed': buildScalarCell(tr.b.Unit.byName.timeDurationInMs,
+ [0.99, 0.999])
+ }
+ }
+ ];
+ }
+
+ function checkCellValue(
+ test, value, expectedText, expectedColor, opt_expectedInfos) {
+ const expectedInfos = opt_expectedInfos || [];
+ assert.lengthOf(Polymer.dom(value).childNodes, 1 + expectedInfos.length);
+ assert.strictEqual(value.style.color, expectedColor);
+ if (typeof expectedText === 'string') {
+ assert.strictEqual(
+ Polymer.dom(Polymer.dom(value).childNodes[0]).textContent,
+ expectedText);
+ } else {
+ expectedText(Polymer.dom(value).childNodes[0]);
+ }
+ for (let i = 0; i < expectedInfos.length; i++) {
+ const expectedInfo = expectedInfos[i];
+ const infoEl = Polymer.dom(value).childNodes[i + 1];
+ assert.strictEqual(Polymer.dom(infoEl).textContent, expectedInfo.icon);
+ assert.strictEqual(infoEl.title, expectedInfo.message);
+ assert.strictEqual(infoEl.style.color, expectedInfo.color || '');
+ }
+ test.addHTMLOutput(value);
+ }
+
+ function sizeSpanMatcher(
+ expectedValue, opt_expectedIsDelta, opt_expectedContext) {
+ return function(element) {
+ assert.strictEqual(element.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(element.value, expectedValue);
+ assert.strictEqual(element.unit, opt_expectedIsDelta ?
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter :
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter);
+ assert.deepEqual(element.context, opt_expectedContext);
+ };
+ }
+
+ test('checkTitleColumn_value', function() {
+ const column = new TitleColumn('column_title');
+ assert.strictEqual(column.title, 'column_title');
+ assert.isFalse(column.supportsCellSelection);
+
+ let row = {title: 'undefined', contexts: undefined};
+ assert.strictEqual(column.formatTitle(row), 'undefined');
+ assert.strictEqual(column.value(row), 'undefined');
+
+ row = {title: 'constant', contexts: [{}, {}, {}, {}]};
+ assert.strictEqual(column.formatTitle(row), 'constant');
+ assert.strictEqual(column.value(row), 'constant');
+
+ row = {title: 'added', contexts: [undefined, undefined, undefined, {}]};
+ assert.strictEqual(column.formatTitle(row), 'added');
+ let value = column.value(row);
+ assert.strictEqual(Polymer.dom(value).textContent, '+++\u00A0added');
+ assert.strictEqual(value.style.color, 'red');
+
+ row = {title: 'removed', contexts: [true, true, undefined, undefined]};
+ assert.strictEqual(column.formatTitle(row), 'removed');
+ value = column.value(row);
+ assert.strictEqual(Polymer.dom(value).textContent, '---\u00A0removed');
+ assert.strictEqual(value.style.color, 'green');
+
+ row = {title: 'flaky', contexts: [true, undefined, true, true]};
+ assert.strictEqual(column.formatTitle(row), 'flaky');
+ value = column.value(row);
+ assert.strictEqual(Polymer.dom(value).textContent, 'flaky');
+ assert.strictEqual(value.style.color, 'purple');
+
+ row = {title: 'added-flaky', contexts: [undefined, {}, undefined, true]};
+ assert.strictEqual(column.formatTitle(row), 'added-flaky');
+ value = column.value(row);
+ assert.strictEqual(Polymer.dom(value).textContent, '+++\u00A0added-flaky');
+ assert.strictEqual(value.style.color, 'purple');
+
+ row = {title: 'removed-flaky', contexts: [true, undefined, {}, undefined]};
+ assert.strictEqual(column.formatTitle(row), 'removed-flaky');
+ value = column.value(row);
+ assert.strictEqual(
+ Polymer.dom(value).textContent, '---\u00A0removed-flaky');
+ assert.strictEqual(value.style.color, 'purple');
+ });
+
+ test('checkTitleColumn_cmp', function() {
+ const column = new TitleColumn('column_title');
+
+ assert.isBelow(column.cmp({title: 'a'}, {title: 'b'}), 0);
+ assert.strictEqual(column.cmp({title: 'cc'}, {title: 'cc'}), 0);
+ assert.isAbove(column.cmp({title: '10'}, {title: '2'}), 0);
+ });
+
+ test('checkMemoryColumn_fromRows', function() {
+ function MockColumn0() {
+ MemoryColumn.apply(this, arguments);
+ }
+ MockColumn0.prototype = {
+ __proto__: MemoryColumn.prototype,
+ get title() { return 'MockColumn0'; }
+ };
+
+ function MockColumn1() {
+ MemoryColumn.apply(this, arguments);
+ }
+ MockColumn1.prototype = {
+ __proto__: MemoryColumn.prototype,
+ get title() { return 'MockColumn1'; }
+ };
+
+ function MockColumn2() {
+ MemoryColumn.apply(this, arguments);
+ }
+ MockColumn2.prototype = {
+ __proto__: MemoryColumn.prototype,
+ get title() { return 'MockColumn2'; }
+ };
+
+ const rules = [
+ {
+ condition: /size/,
+ importance: 10,
+ columnConstructor: MockColumn0
+ },
+ {
+ condition: 'cpu_temperature',
+ importance: 0,
+ columnConstructor: MockColumn1
+ },
+ {
+ condition: 'unmatched',
+ importance: -1,
+ get columnConstructor() {
+ throw new Error('The constructor should never be retrieved');
+ }
+ },
+ {
+ importance: 1,
+ columnConstructor: MockColumn2
+ }
+ ];
+
+ const rows = buildTestRows();
+ const columns = MemoryColumn.fromRows(rows, {
+ cellKey: 'fields',
+ aggregationMode: AggregationMode.MAX,
+ rules,
+ shouldSetContextGroup: true
+ });
+ assert.lengthOf(columns, 4);
+
+ const pageSizeColumn = columns[0];
+ assert.strictEqual(pageSizeColumn.name, 'page_size');
+ assert.strictEqual(pageSizeColumn.title, 'MockColumn0');
+ assert.strictEqual(pageSizeColumn.aggregationMode, AggregationMode.MAX);
+ assert.strictEqual(pageSizeColumn.cell({fields: {page_size: 'large'}}),
+ 'large');
+ assert.isTrue(pageSizeColumn.shouldSetContextGroup);
+ assert.instanceOf(pageSizeColumn, MockColumn0);
+
+ const mixedColumn = columns[1];
+ assert.strictEqual(mixedColumn.name, 'mixed');
+ assert.strictEqual(mixedColumn.title, 'MockColumn2');
+ assert.strictEqual(mixedColumn.aggregationMode, AggregationMode.MAX);
+ assert.strictEqual(mixedColumn.cell({fields: {mixed: 89}}), 89);
+ assert.isTrue(mixedColumn.shouldSetContextGroup);
+ assert.instanceOf(mixedColumn, MockColumn2);
+
+ const mixed2Column = columns[2];
+ assert.strictEqual(mixed2Column.name, 'mixed2');
+ assert.strictEqual(mixed2Column.title, 'MockColumn2');
+ assert.strictEqual(mixed2Column.aggregationMode, AggregationMode.MAX);
+ assert.strictEqual(mixed2Column.cell({fields: {mixed2: 'invalid'}}),
+ 'invalid');
+ assert.isTrue(mixed2Column.shouldSetContextGroup);
+ assert.instanceOf(mixed2Column, MockColumn2);
+
+ const cpuTemperatureColumn = columns[3];
+ assert.strictEqual(cpuTemperatureColumn.name, 'cpu_temperature');
+ assert.strictEqual(cpuTemperatureColumn.title, 'MockColumn1');
+ assert.strictEqual(cpuTemperatureColumn.aggregationMode,
+ AggregationMode.MAX);
+ assert.strictEqual(
+ cpuTemperatureColumn.cell({fields: {cpu_temperature: 42}}), 42);
+ assert.isTrue(cpuTemperatureColumn.shouldSetContextGroup);
+ assert.instanceOf(cpuTemperatureColumn, MockColumn1);
+ });
+
+ test('checkMemoryColumn_spaceEqually', function() {
+ // Zero columns.
+ let columns = [];
+ MemoryColumn.spaceEqually(columns);
+
+ // One column.
+ columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; }
+ }
+ ];
+ MemoryColumn.spaceEqually(columns);
+ checkPercent(columns[0].width, 100);
+
+ // Two columns.
+ columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; }
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.firstData; }
+ }
+ ];
+ MemoryColumn.spaceEqually(columns);
+ checkPercent(columns[0].width, 50);
+ checkPercent(columns[1].width, 50);
+ });
+
+ test('checkMemoryColumn_instantiate', function() {
+ const c = new MemoryColumn('test_column', ['x'], AggregationMode.MAX);
+ assert.strictEqual(c.name, 'test_column');
+ assert.strictEqual(c.title, 'test_column');
+ assert.strictEqual(c.cell({x: 95}), 95);
+ assert.isUndefined(c.width);
+ assert.isUndefined(c.color());
+ });
+
+ test('checkMemoryColumn_cell', function() {
+ const c = new MemoryColumn('test_column', ['a', 'b'], AggregationMode.MAX);
+ const cell = new MemoryCell(undefined);
+
+ assert.isUndefined(c.cell(undefined));
+ assert.isUndefined(c.cell({b: cell}));
+ assert.isUndefined(c.cell({a: {c: cell}}));
+ assert.strictEqual(c.cell({a: {b: cell, c: 42}}), cell);
+ });
+
+ test('checkMemoryColumn_fields', function() {
+ const c = new MemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+
+ // Undefined cell or field inside cell.
+ assert.isUndefined(c.fields({}));
+ assert.isUndefined(c.fields({x: new MemoryCell(undefined)}));
+
+ // Defined field(s) inside cell.
+ const field1 = new Scalar(tr.b.Unit.byName.powerInWatts, 1013.25);
+ const field2 = new Scalar(tr.b.Unit.byName.powerInWatts, 1065);
+ const row1 = {x: new MemoryCell([field1])};
+ const row2 = {x: new MemoryCell([field1, field2])};
+ assert.deepEqual(c.fields(row1), [field1]);
+ assert.deepEqual(c.fields(row2), [field1, field2]);
+ });
+
+ test('checkMemoryColumn_hasAllRelevantFieldsUndefined', function() {
+ // Single field.
+ const c1 = new MemoryColumn('single_column', ['x'],
+ undefined /* aggregation mode */);
+ assert.isTrue(c1.hasAllRelevantFieldsUndefined([undefined]));
+ assert.isFalse(c1.hasAllRelevantFieldsUndefined(
+ [new Scalar(sizeInBytes_smallerIsBetter, 16)]));
+
+ // Multiple fields, diff aggregation mode.
+ const c2 = new MemoryColumn('diff_column', ['x'],
+ AggregationMode.DIFF);
+ assert.isTrue(c2.hasAllRelevantFieldsUndefined([undefined, undefined]));
+ assert.isTrue(c2.hasAllRelevantFieldsUndefined(
+ [undefined, undefined, undefined]));
+ assert.isTrue(c2.hasAllRelevantFieldsUndefined(
+ [undefined, new Scalar(sizeInBytes_smallerIsBetter, 16), undefined]));
+ assert.isFalse(c2.hasAllRelevantFieldsUndefined(
+ [undefined, new Scalar(sizeInBytes_smallerIsBetter, 32)]));
+ assert.isFalse(c2.hasAllRelevantFieldsUndefined(
+ [new Scalar(sizeInBytes_smallerIsBetter, 32), undefined, undefined]));
+ assert.isFalse(c2.hasAllRelevantFieldsUndefined([
+ new Scalar(sizeInBytes_smallerIsBetter, 16),
+ undefined,
+ new Scalar(sizeInBytes_smallerIsBetter, 32)
+ ]));
+
+ // Multiple fields, max aggregation mode.
+ const c3 = new MemoryColumn('max_column', ['x'],
+ AggregationMode.MAX);
+ assert.isTrue(c3.hasAllRelevantFieldsUndefined([undefined, undefined]));
+ assert.isTrue(c3.hasAllRelevantFieldsUndefined(
+ [undefined, undefined, undefined]));
+ assert.isFalse(c3.hasAllRelevantFieldsUndefined(
+ [undefined, new Scalar(sizeInBytes_smallerIsBetter, 16), undefined]));
+ assert.isFalse(c3.hasAllRelevantFieldsUndefined(
+ [undefined, new Scalar(sizeInBytes_smallerIsBetter, 32)]));
+ assert.isFalse(c3.hasAllRelevantFieldsUndefined([
+ new Scalar(sizeInBytes_smallerIsBetter, 32),
+ undefined,
+ new Scalar(sizeInBytes_smallerIsBetter, 16)
+ ]));
+ });
+
+ test('checkMemoryColumn_value_allFieldsUndefined', function() {
+ const c1 = new MemoryColumn('no_color', ['x'],
+ AggregationMode.MAX);
+ const c2 = new MemoryColumn('color', ['x'],
+ AggregationMode.DIFF);
+ Object.defineProperty(c2, 'color', {
+ get() {
+ throw new Error('The color should never be retrieved');
+ }
+ });
+
+ // Infos should be completely ignored.
+ c1.addInfos = c2.addInfos = function() {
+ throw new Error('This method should never be called');
+ };
+
+ [c1, c2].forEach(function(c) {
+ assert.strictEqual(c.value({}), '');
+ assert.strictEqual(c.value({x: new MemoryCell(undefined)}), '');
+ assert.strictEqual(c.value({x: new MemoryCell([undefined])}), '');
+ assert.strictEqual(
+ c.value({x: new MemoryCell([undefined, undefined])}), '');
+ });
+
+ // Diff should only take into account the first and last field value.
+ assert.strictEqual(c2.value({
+ x: new MemoryCell([
+ undefined,
+ new Scalar(sizeInBytes_smallerIsBetter, 16),
+ undefined
+ ])
+ }), '');
+ });
+
+ test('checkMemoryColumn_getImportance', function() {
+ const c = new NumericMemoryColumn('test_column', ['x']);
+
+ const rules1 = [];
+ assert.strictEqual(c.getImportance(rules1), 0);
+
+ const rules2 = [
+ {
+ condition: 'test',
+ importance: 4
+ },
+ {
+ condition: /test$/,
+ importance: 2
+ }
+ ];
+ assert.strictEqual(c.getImportance(rules2), 1);
+
+ const rules3 = [
+ {
+ condition: 'test_column',
+ importance: 10
+ },
+ {
+ importance: 5
+ }
+ ];
+ assert.strictEqual(c.getImportance(rules3), 10);
+
+ const rules4 = [
+ {
+ condition: 'test_column2',
+ importance: 8
+ },
+ {
+ condition: /column/,
+ importance: 12
+ }
+ ];
+ assert.strictEqual(c.getImportance(rules4), 12);
+ });
+
+ test('checkMemoryColumn_nameMatchesCondition', function() {
+ const c = new NumericMemoryColumn('test_column', ['x']);
+
+ assert.isTrue(MemoryColumn.nameMatchesCondition('test_column', undefined));
+
+ assert.isFalse(MemoryColumn.nameMatchesCondition('test_column', 'test'));
+ assert.isTrue(
+ MemoryColumn.nameMatchesCondition('test_column', 'test_column'));
+ assert.isFalse(
+ MemoryColumn.nameMatchesCondition('test_column', 'test_column2'));
+
+ assert.isTrue(MemoryColumn.nameMatchesCondition('test_column', /test/));
+ assert.isTrue(
+ MemoryColumn.nameMatchesCondition('test_column', /^[^_]*_[^_]*$/));
+ assert.isFalse(MemoryColumn.nameMatchesCondition('test_column', /test$/));
+ });
+
+ test('checkStringMemoryColumn_value_singleField', function() {
+ const c = new StringMemoryColumn('', ['x'], AggregationMode.MAX);
+ c.color = function(fields, contexts) {
+ if (fields[0] < '0') return 'green';
+ if (contexts && contexts[0] % 2 === 0) return 'red';
+ return undefined;
+ };
+
+ const infos1 = [{ icon: '\u{1F648}', message: 'Some info', color: 'blue' }];
+ const infos2 = [
+ { icon: '\u{1F649}', message: 'Start', color: 'cyan' },
+ { icon: '\u{1F64A}', message: 'Stop' }
+ ];
+ c.addInfos = function(fields, contexts, infos) {
+ if (fields[0] < '0') {
+ infos.push.apply(infos, infos1);
+ } else if (contexts && contexts[0] % 2 === 0) {
+ infos.push.apply(infos, infos2);
+ }
+ };
+
+ let row = {x: new MemoryCell(['123'])};
+ assert.strictEqual(c.value(row), '123');
+
+ row = {x: new MemoryCell(['-123']), contexts: [undefined]};
+ checkCellValue(this, c.value(row), '-123', 'green', infos1);
+
+ row = {x: new MemoryCell(['123']), contexts: [42]};
+ checkCellValue(this, c.value(row), '123', 'red', infos2);
+ });
+
+ test('checkStringMemoryColumn_value_multipleFields', function() {
+ const c1 = new StringMemoryColumn('test_column1', ['x'],
+ undefined /* aggregation mode */);
+ const c2 = new StringMemoryColumn('test_column2', ['x'],
+ AggregationMode.DIFF);
+ c2.color = function(fields, contexts) {
+ return '#009999';
+ };
+ const c3 = new StringMemoryColumn('test_column3', ['x'],
+ AggregationMode.MAX);
+ c3.color = function(fields, contexts) {
+ if (fields[0] < '0') {
+ return 'green';
+ } else if (contexts && contexts[contexts.length - 1] % 2 === 0) {
+ return 'red';
+ }
+ return undefined;
+ };
+
+ const infos1 = [{ icon: '\u{1F648}', message: 'Some info', color: 'blue' }];
+ const infos2 = [
+ { icon: '\u{1F649}', message: 'Start', color: 'cyan' },
+ { icon: '\u{1F64A}', message: 'Stop' }
+ ];
+ c1.addInfos = c2.addInfos = c3.addInfos =
+ function(fields, contexts, infos) {
+ if (fields[0] < '0') {
+ infos.push.apply(infos, infos1);
+ } else if (contexts && contexts[contexts.length - 1] % 2 === 0) {
+ infos.push.apply(infos, infos2);
+ }
+ };
+
+ let row = {x: new MemoryCell(['123', '456'])};
+ checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', '');
+ checkCellValue(this, c2.value(row), '123 \u2192 456', 'rgb(0, 153, 153)');
+ assert.strictEqual(c3.value(row), '456');
+
+ row = {
+ x: new MemoryCell(['-123', undefined, '+123']),
+ contexts: [12, 14, undefined]
+ };
+ checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', '',
+ infos1);
+ checkCellValue(this, c2.value(row), '-123 \u2192 +123', 'rgb(0, 153, 153)',
+ infos1);
+ checkCellValue(this, c3.value(row), '+123', 'green', infos1);
+
+ row = {
+ x: new MemoryCell(['123', undefined, '456']),
+ contexts: [31, 7, -2]
+ };
+ checkCellValue(this, c1.value(row), '(unsupported aggregation mode)', '',
+ infos2);
+ checkCellValue(this, c2.value(row), '123 \u2192 456', 'rgb(0, 153, 153)',
+ infos2);
+ checkCellValue(this, c3.value(row), '456', 'red', infos2);
+ });
+
+ test('checkStringMemoryColumn_formatSingleField', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ undefined /* aggregation mode */);
+
+ assert.strictEqual(c.formatSingleField('1024'), '1024');
+ assert.strictEqual(c.formatSingleField('~10'), '~10');
+ });
+
+ test('checkStringMemoryColumn_formatMultipleFields_diff', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+
+ // Added value.
+ checkMemoryColumnFieldFormat(this, c, [undefined, 'few'], '+few', 'red');
+ checkMemoryColumnFieldFormat(this, c, [undefined, 64, 32], '+32', 'red');
+
+ // Removed value.
+ checkMemoryColumnFieldFormat(this, c, ['00', undefined], '-00', 'green');
+ checkMemoryColumnFieldFormat(this, c, [1, undefined, 2, undefined], '-1',
+ 'green');
+
+ // Identical values.
+ checkMemoryColumnFieldFormat(this, c, ['Unchanged', 'Unchanged'],
+ 'Unchanged', undefined /* unchanged color (not an HTML element) */);
+ checkMemoryColumnFieldFormat(this, c, [16, 32, undefined, 64, 16], '16',
+ undefined /* unchanged color (not an HTML element) */);
+
+ // Different values.
+ checkMemoryColumnFieldFormat(this, c, ['A', 'C', undefined, 'C', 'B'],
+ 'A \u2192 B', 'darkorange');
+ checkMemoryColumnFieldFormat(this, c, [16, undefined, 64], '16 \u2192 64',
+ 'darkorange');
+ });
+
+ test('checkStringMemoryColumn_formatMultipleFields_max', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+
+ // Different values.
+ checkMemoryColumnFieldFormat(this, c, ['A', 'B', 'A'], 'B',
+ undefined /* unchanged color (not an HTML element) */);
+ checkMemoryColumnFieldFormat(this, c, [16, 16, undefined, 17], '17',
+ undefined /* unchanged color (not an HTML element) */);
+
+ // Identical values.
+ checkMemoryColumnFieldFormat(this, c, ['X', 'X'], 'X',
+ undefined /* unchanged color (not an HTML element) */);
+ checkMemoryColumnFieldFormat(this, c, [7, undefined, 7, undefined, 7], '7',
+ undefined /* unchanged color (not an HTML element) */);
+ });
+
+ test('checkStringMemoryColumn_compareSingleFields', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ undefined /* aggregation mode */);
+
+ assert.isBelow(c.compareSingleFields(
+ new Scalar(sizeInBytes_smallerIsBetter, 2),
+ new Scalar(sizeInBytes_smallerIsBetter, 10)), 0);
+ assert.strictEqual(c.compareSingleFields('equal', 'equal'), 0);
+ assert.isAbove(c.compareSingleFields('100', '99'), 0);
+ });
+
+ test('checkStringMemoryColumn_compareMultipleFields_diff', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+
+ // One field was added.
+ checkCompareFieldsLess(c, [-10, 10], [undefined, 5]);
+ checkCompareFieldsLess(c,
+ [-100, undefined, undefined], [undefined, 4, 5]);
+ checkCompareFieldsLess(c,
+ [1, 2, 3, 4], [undefined, 'x', undefined, 'y']);
+
+ // Both fields were added.
+ checkCompareFieldsEqual(c,
+ [undefined, 'C', undefined, 'A'], [undefined, 'B', 'D', 'A']);
+ checkCompareFieldsLess(c, [undefined, 1], [undefined, 2]);
+ checkCompareFieldsLess(c, [undefined, 6, 3], [undefined, 5, 4]);
+
+ // One field was removed (neither was added).
+ checkCompareFieldsLess(c, ['B', undefined], ['A', 'A']);
+ checkCompareFieldsLess(c,
+ [5, undefined, undefined], [undefined, -5, -10]);
+
+ // Both fields were removed (neither was added)
+ checkCompareFieldsEqual(c, ['T', 'A', undefined, undefined],
+ ['T', 'B', 'C', undefined]);
+ checkCompareFieldsLess(c, [5, undefined], [4, undefined]);
+
+ // Neither field was added or removed.
+ checkCompareFieldsLess(c, ['BB', 'BB'], ['AA', 'CC']);
+ checkCompareFieldsEqual(c, [7, 8, 9], [6, 9, 10]);
+ checkCompareFieldsEqual(c, [5, undefined, 5], [4, 3, 4]);
+ });
+
+ test('checkStringMemoryColumn_compareMultipleFields_max', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+
+ // At least one field has multiple values.
+ checkCompareFieldsEqual(c, [0, 1, 3], [1, 3, 2]);
+ checkCompareFieldsLess(c, ['4', undefined, '4'], ['3', '4', '5']);
+ checkCompareFieldsLess(c, [3, 3, 3], [9, undefined, 10]);
+
+ // Both fields have single values.
+ checkCompareFieldsEqual(c,
+ [undefined, 'ttt', undefined], ['ttt', 'ttt', undefined]);
+ checkCompareFieldsLess(c, [undefined, -1, undefined], [-2, -2, -2]);
+ checkCompareFieldsLess(c, ['Q', 'Q', undefined], ['X', undefined, 'X']);
+ });
+
+ test('checkStringMemoryColumn_cmp', function() {
+ const c = new StringMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+
+ // Cell (or the associated field) undefined in one or both rows.
+ assert.strictEqual(c.cmp({}, {y: new MemoryCell([undefined])}), 0);
+ assert.strictEqual(c.cmp({x: new MemoryCell(undefined)}, {}), 0);
+ assert.strictEqual(
+ c.cmp({x: new MemoryCell([undefined, undefined])}, {}), 0);
+ assert.isAbove(c.cmp({x: new MemoryCell(['negative'])}, {}), 0);
+ assert.isAbove(c.cmp({x: new MemoryCell(['negative'])},
+ {x: new MemoryCell([undefined])}), 0);
+ assert.isBelow(c.cmp({}, {x: new MemoryCell(['positive'])}), 0);
+ assert.isBelow(c.cmp({x: new MemoryCell(undefined)},
+ {x: new MemoryCell(['positive'])}), 0);
+
+ // Single field.
+ assert.strictEqual(c.cmp({x: new MemoryCell(['equal'])},
+ {x: new MemoryCell(['equal'])}), 0);
+ assert.isAbove(c.cmp({x: new MemoryCell(['bigger'])},
+ {x: new MemoryCell(['BIG'])}), 0);
+ assert.isBelow(c.cmp({x: new MemoryCell(['small'])},
+ {x: new MemoryCell(['smaLL'])}), 0);
+
+ // Multiple fields.
+ assert.isBelow(c.cmp(
+ {x: new MemoryCell(['MemoryColumn', 'supports*', undefined])},
+ {x: new MemoryCell(['comparing', 'multiple', 'values :-)'])}), 0);
+ });
+
+ test('checkNumericMemoryColumn_value', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+ c.color = function(fields, contexts) {
+ return '#009999';
+ };
+ const infos1 = [createWarningInfo('Attention!')];
+ c.addInfos = function(fields, contexts, infos) {
+ infos.push.apply(infos, infos1);
+ };
+
+ // Undefined field values.
+ let row = {x: buildScalarCell(sizeInBytes_smallerIsBetter,
+ [undefined, 1, undefined])};
+ assert.strictEqual(c.value(row), '');
+
+ // Single field value.
+ row = {x: buildScalarCell(sizeInBytes_smallerIsBetter,
+ [5.4975581e13/* 50 TiB */])};
+ checkCellValue(this, c.value(row), sizeSpanMatcher(5.4975581e13),
+ 'rgb(0, 153, 153)', infos1);
+
+ // Multiple field values.
+ row = {
+ x: buildScalarCell(sizeInBytes_smallerIsBetter,
+ [5.4975581e13/* 50 TiB */, undefined, 2.1990233e13/* 20 TiB */])
+ };
+ checkCellValue(this, c.value(row),
+ sizeSpanMatcher(-3.2985348e13, true /* opt_expectedIsDelta */),
+ 'rgb(0, 153, 153)', infos1);
+
+ // With custom formatting context.
+ c.getFormattingContext = function(unit) {
+ assert.strictEqual(unit,
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter);
+ return { minimumFractionDigits: 2 };
+ };
+ checkCellValue(this, c.value(row),
+ sizeSpanMatcher(-3.2985348e13, true /* opt_expectedIsDelta */,
+ { minimumFractionDigits: 2 }),
+ 'rgb(0, 153, 153)', infos1);
+ });
+
+ test('checkNumericMemoryColumn_formatSingleField', function() {
+ let c = new NumericMemoryColumn('non_bytes_column', ['x'],
+ undefined /* aggregation mode */);
+ let value = c.formatSingleField(new Scalar(
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 123));
+ assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(value.value, 123);
+ assert.strictEqual(value.unit,
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter);
+ assert.isUndefined(value.contextGroup);
+ this.addHTMLOutput(value);
+
+ c = new NumericMemoryColumn('bytes_column', ['x'],
+ undefined /* aggregation mode */);
+ c.shouldSetContextGroup = true;
+ value = c.formatSingleField(new Scalar(
+ sizeInBytes_smallerIsBetter, 456));
+ assert.strictEqual(value.tagName, 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(value.value, 456);
+ assert.strictEqual(value.unit,
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter);
+ assert.strictEqual(value.contextGroup, 'bytes_column');
+ this.addHTMLOutput(value);
+ });
+
+ test('checkNumericMemoryColumn_formatMultipleFields_diff',
+ function() {
+ let c = new NumericMemoryColumn(
+ 'non_bytes_column', ['x'], AggregationMode.DIFF);
+ checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3],
+ tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 2);
+ checkNumericMemoryColumnFieldFormat(this, c, [10, undefined],
+ tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, -10);
+ checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0],
+ tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 0);
+ checkNumericMemoryColumnFieldFormat(
+ this, c, [2.71828, 2.71829] /* diff within epsilon */,
+ tr.b.Unit.byName.unitlessNumberDelta_smallerIsBetter, 0);
+
+ c = new NumericMemoryColumn(
+ 'bytes_column', ['x'], AggregationMode.DIFF);
+ checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3],
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 2);
+ checkNumericMemoryColumnFieldFormat(this, c, [10, undefined],
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, -10);
+ checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0],
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 0);
+ checkNumericMemoryColumnFieldFormat(
+ this, c, [1.41421, 1.41422] /* diff within epsilon */,
+ tr.b.Unit.byName.sizeInBytesDelta_smallerIsBetter, 0);
+ });
+
+ test('checkNumericMemoryColumn_formatMultipleFields_max',
+ function() {
+ let c = new NumericMemoryColumn(
+ 'non_bytes_column', ['x'], AggregationMode.MAX);
+ checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3],
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 3);
+ checkNumericMemoryColumnFieldFormat(this, c, [10, undefined],
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 10);
+ checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0],
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 60);
+ checkNumericMemoryColumnFieldFormat(
+ this, c, [undefined, 10, 20, undefined],
+ tr.b.Unit.byName.unitlessNumber_smallerIsBetter, 20);
+
+ c = new NumericMemoryColumn(
+ 'bytes_column', ['x'], AggregationMode.MAX);
+ checkNumericMemoryColumnFieldFormat(this, c, [1, 2, 3],
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 3);
+ checkNumericMemoryColumnFieldFormat(this, c, [10, undefined],
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 10);
+ checkNumericMemoryColumnFieldFormat(this, c, [undefined, 60, 0],
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 60);
+ checkNumericMemoryColumnFieldFormat(
+ this, c, [undefined, 10, 20, undefined],
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter, 20);
+ });
+
+ test('checkNumericMemoryColumn_cmp', function() {
+ const c = new NumericMemoryColumn(
+ 'test_column', ['x'], AggregationMode.DIFF);
+
+ // Undefined field values.
+ assert.isAbove(c.cmp({x: buildScalarCell(sizeInBytes_smallerIsBetter,
+ [-9999999999])},
+ {x: undefined}), 0);
+ assert.isBelow(c.cmp({x: new MemoryCell(undefined)},
+ {x: buildScalarCell(sizeInBytes_smallerIsBetter, [748, 749])}), 0);
+ assert.strictEqual(
+ c.cmp({}, {
+ x: buildScalarCell(
+ sizeInBytes_smallerIsBetter, [undefined, undefined])
+ }), 0);
+
+ // Single field value.
+ assert.isBelow(c.cmp(
+ {x: buildScalarCell(sizeInBytes_smallerIsBetter, [16384])},
+ {x: buildScalarCell(sizeInBytes_smallerIsBetter, [32768])}), 0);
+
+ // Multiple field values.
+ assert.strictEqual(c.cmp(
+ {x: buildScalarCell(
+ sizeInBytes_smallerIsBetter, [999, undefined, 1001])},
+ {x: buildScalarCell(
+ sizeInBytes_smallerIsBetter, [undefined, 5, 2])}), 0);
+ });
+
+ test('checkNumericMemoryColumn_compareSingleFields', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ undefined /* aggregation mode */);
+
+ assert.isBelow(c.compareSingleFields(
+ new Scalar(
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, 99),
+ new Scalar(
+ tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, 100)), 0);
+ assert.strictEqual(c.compareSingleFields(
+ new Scalar(tr.b.Unit.byName.unitlessNumber, 0xEEE),
+ new Scalar(tr.b.Unit.byName.unitlessNumber, 0xEEE)), 0);
+ assert.isAbove(c.compareSingleFields(
+ new Scalar(sizeInBytes_smallerIsBetter, 10),
+ new Scalar(sizeInBytes_smallerIsBetter, 2)), 0);
+ });
+
+ test('checkNumericMemoryColumn_compareMultipleFields_diff', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ AggregationMode.DIFF);
+
+ assert.isBelow(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [10000, 10001, 10002] /* diff +2 */).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [5, 7, 8] /* diff +3 */).fields), 0);
+ assert.strictEqual(c.compareMultipleFields(
+ buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [4, undefined] /* diff -4 */).fields,
+ buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [999, 995] /* diff -4 */).fields), 0);
+ assert.isAbove(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [10, undefined, 12] /* diff +2 */).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [11, 50, 12] /* diff +1 */).fields), 0);
+ assert.strictEqual(c.compareMultipleFields(
+ buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter,
+ [17, undefined, 17] /* diff 0 */).fields,
+ buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter,
+ [undefined, 100, undefined] /* diff 0 */).fields), 0);
+ assert.strictEqual(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [3.14159, undefined, 3.14160] /* diff within epsilon */).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [100, 100, 100] /* diff 0 */).fields), 0);
+ });
+
+ test('checkNumericMemoryColumn_compareMultipleFields_max', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+
+ assert.isBelow(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [10, undefined, 12]).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter, [11, 50, 12]).fields), 0);
+ assert.strictEqual(c.compareMultipleFields(
+ buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [999, undefined, -8888]).fields,
+ buildScalarCell(tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
+ [undefined, 999, undefined]).fields), 0);
+ assert.isAbove(c.compareMultipleFields(
+ buildScalarCell(sizeInBytes_smallerIsBetter,
+ [10000, 10001, 10002]).fields,
+ buildScalarCell(sizeInBytes_smallerIsBetter, [5, 7, 8]).fields), 0);
+ assert.isBelow(c.compareMultipleFields(
+ buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter,
+ [17, undefined, 17]).fields,
+ buildScalarCell(tr.b.Unit.byName.powerInWatts_smallerIsBetter,
+ [undefined, 100, undefined]).fields), 0);
+ });
+
+ test('checkNumericMemoryColumn_getDiffFieldValue', function() {
+ const c = new NumericMemoryColumn('test_column', ['x'],
+ AggregationMode.MAX);
+ function checkDiffValue(first, last, expectedDiffValue) {
+ const actualDiffValue = c.getDiffFieldValue_(
+ first === undefined ? undefined :
+ new Scalar(sizeInBytes_smallerIsBetter, first),
+ last === undefined ? undefined :
+ new Scalar(sizeInBytes_smallerIsBetter, last));
+ assert.closeTo(actualDiffValue, expectedDiffValue, 1e-8);
+ }
+
+ // Diff outside epsilon range.
+ checkDiffValue(0, 0.0002, 0.0002);
+ checkDiffValue(undefined, 0.0003, 0.0003);
+ checkDiffValue(0.3334, 0.3332, -0.0002);
+ checkDiffValue(0.0005, undefined, -0.0005);
+
+ // Diff inside epsilon range.
+ checkDiffValue(5, 5.00009, 0);
+ checkDiffValue(undefined, 0.0000888, 0);
+ checkDiffValue(0.29999, 0.3, 0);
+ checkDiffValue(0.00009, undefined, 0);
+ checkDiffValue(0.777777, 0.777777, 0);
+ checkDiffValue(undefined, undefined, 0);
+ });
+
+ test('checkExpandTableRowsRecursively', function() {
+ const columns = [
+ {
+ title: 'Single column',
+ value(row) { return row.data; },
+ width: '100px'
+ }
+ ];
+
+ const rows = [
+ {
+ data: 'allocated',
+ subRows: [
+ {
+ data: 'v8',
+ subRows: []
+ },
+ {
+ data: 'oilpan',
+ subRows: [
+ {
+ data: 'still_visible',
+ subRows: [
+ {
+ data: 'not_visible_any_more'
+ }
+ ]
+ },
+ {
+ data: 'also_visible'
+ }
+ ]
+ }
+ ]
+ },
+ {
+ data: 'no_sub_rows'
+ },
+ {
+ data: 'fragmentation',
+ subRows: [
+ {
+ data: 'internal'
+ },
+ {
+ data: 'external',
+ subRows: [
+ {
+ data: 'unexpanded'
+ }
+ ]
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ expandTableRowsRecursively(table);
+
+ function isExpanded(row) { return table.getExpandedForTableRow(row); }
+
+ // Level 0 (3 rows) should be expanded (except for nodes which have no
+ // sub-rows).
+ assert.isTrue(isExpanded(rows[0] /* allocated */));
+ assert.isFalse(isExpanded(rows[1] /* no_sub_rows */));
+ assert.isTrue(isExpanded(rows[2] /* overhead */));
+
+ // Level 1 (4 rows) should be expanded (except for nodes which have no
+ // sub-rows).
+ assert.isFalse(isExpanded(rows[0].subRows[0] /* allocated/v8 */));
+ assert.isTrue(isExpanded(rows[0].subRows[1] /* allocated/oilpan */));
+ assert.isFalse(isExpanded(rows[2].subRows[0] /* fragmentation/internal */));
+ assert.isTrue(isExpanded(rows[2].subRows[1] /* fragmentation/external */));
+
+ // Level 2 (3 rows) should not be expanded any more.
+ assert.isFalse(isExpanded(
+ rows[0].subRows[1].subRows[0] /* allocated/oilpan/still_visible */));
+ assert.isFalse(isExpanded(
+ rows[0].subRows[1].subRows[1] /* allocated/oilpan/also_visible */));
+ assert.isFalse(isExpanded(
+ rows[2].subRows[1].subRows[0] /* fragmentation/external/unexpanded */));
+ });
+
+ test('checkMemoryCell_extractFields', function() {
+ assert.isUndefined(MemoryCell.extractFields(undefined));
+
+ assert.isUndefined(MemoryCell.extractFields(new MemoryCell(undefined)));
+
+ const fields = [new Scalar(sizeInBytes_smallerIsBetter, 1024)];
+ assert.strictEqual(
+ MemoryCell.extractFields(new MemoryCell(fields)), fields);
+ });
+
+ test('checkAggregateTableRowCellsRecursively', function() {
+ const row = {
+ testCells: {
+ a: buildScalarCell(sizeInBytes_smallerIsBetter, [17])
+ },
+ subRows: [
+ {
+ // Intentionally no testCells.
+ subRows: [
+ {
+ testCells: {
+ b: buildScalarCell(sizeInBytes_smallerIsBetter, [103]),
+ c: new MemoryCell(['should-not-propagate-upwards']),
+ d: buildScalarCell(sizeInBytes_smallerIsBetter, [-200])
+ }
+ // Intentionally no subRows.
+ },
+ {
+ testCells: {},
+ subRows: []
+ }
+ ],
+ contexts: ['skip-row-when-using-predicate']
+ },
+ {
+ testCells: {
+ b: buildScalarCell(sizeInBytes_smallerIsBetter, [20]),
+ a: buildScalarCell(sizeInBytes_smallerIsBetter, [13]),
+ e: buildScalarCell(sizeInBytes_smallerIsBetter, [-300])
+ },
+ contexts: ['don\'t-skip']
+ }
+ ]
+ };
+
+ // Without a predicate.
+ const ca = new NumericMemoryColumn('column_a', ['testCells', 'a']);
+ const cb = new NumericMemoryColumn('column_b', ['testCells', 'b']);
+ const cc = new StringMemoryColumn('column_c', ['testCells', 'c']);
+ aggregateTableRowCellsRecursively(row, [ca, cb, cc]);
+ checkSizeNumericFields(row, ca, [17]);
+ checkSizeNumericFields(row, cb, [123]);
+ checkStringFields(row, cc, undefined);
+
+ // With a predicate.
+ const cd = new NumericMemoryColumn('column_d', ['testCells', 'd']);
+ const ce = new NumericMemoryColumn('column_e', ['testCells', 'e']);
+ aggregateTableRowCellsRecursively(row, [cd, ce], function(contexts) {
+ return contexts === undefined || !contexts[0].startsWith('skip');
+ });
+ checkSizeNumericFields(row, cd, undefined);
+ checkSizeNumericFields(row, ce, [-300]);
+ });
+
+ test('checkAggregateTableRowCells', function() {
+ const row = {
+ // Intentionally no testCells.
+ otherCells: {
+ a: buildScalarCell(tr.b.Unit.byName.unitlessNumber,
+ [5, undefined, undefined])
+ }
+ };
+ const subRows = [
+ {
+ testCells: {
+ a: buildScalarCell(sizeInBytes_smallerIsBetter, [1, 9])
+ },
+ subRows: [
+ {
+ testCells: {
+ c: buildScalarCell(sizeInBytes_smallerIsBetter, [13])
+ }
+ }
+ ]
+ },
+ {
+ testCells: {
+ a: buildScalarCell(sizeInBytes_smallerIsBetter, [2, 17]),
+ b: buildScalarCell(sizeInBytes_smallerIsBetter, [5])
+ },
+ otherCells: {
+ a: buildScalarCell(tr.b.Unit.byName.unitlessNumber,
+ [153, undefined, 257]),
+ b: new MemoryCell(['field-should-not-propagate-upwards', ''])
+ }
+ }
+ ];
+
+ const cta = new NumericMemoryColumn('column_test_a', ['testCells', 'a']);
+ const ctb = new NumericMemoryColumn('column_test_b', ['testCells', 'b']);
+ const ctc = new NumericMemoryColumn('column_test_c', ['testCells', 'c']);
+ const coa = new NumericMemoryColumn('column_other_a', ['otherCells', 'a']);
+ const cob = new StringMemoryColumn('column_other_b', ['otherCells', 'b']);
+
+ aggregateTableRowCells(row, subRows, [cta, ctb, ctc, coa, cob]);
+
+ checkSizeNumericFields(row, cta, [3, 26]);
+ checkSizeNumericFields(row, ctb, [5]);
+ checkSizeNumericFields(row, ctc, undefined);
+
+ checkNumericFields(row, coa, [5, undefined, 257],
+ tr.b.Unit.byName.unitlessNumber);
+ checkStringFields(row, cob, undefined);
+ });
+
+ test('checkCreateCells', function() {
+ const values = [
+ {
+ a: 9,
+ b: 314
+ },
+ {
+ b: 159,
+ c: undefined
+ },
+ undefined,
+ {
+ b: 265,
+ d: 0
+ }
+ ];
+
+ const mockColumn = new MemoryColumn('', [], undefined);
+
+ const cells = createCells(values, function(dict) {
+ const fields = {};
+ for (const [key, value] of Object.entries(dict)) {
+ if (value === undefined) continue;
+ fields[key] = new Scalar(sizeInBytes_smallerIsBetter, value);
+ }
+ return fields;
+ });
+ assert.deepEqual(Object.keys(cells), ['a', 'b', 'd']);
+ checkSizeNumericFields(
+ cells.a, mockColumn, [9, undefined, undefined, undefined]);
+ checkSizeNumericFields(cells.b, mockColumn, [314, 159, undefined, 265]);
+ checkSizeNumericFields(
+ cells.d, mockColumn, [undefined, undefined, undefined, 0]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html
new file mode 100644
index 00000000000..2a20bb3c27e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html
@@ -0,0 +1,382 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-memory-dump-vm-regions-details-pane'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #label {
+ flex: 0 0 auto;
+ padding: 8px;
+
+ background-color: #eee;
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ #contents {
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+
+ #info_text {
+ padding: 8px;
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ #table {
+ display: none; /* Hide until memory dumps are set. */
+ flex: 1 0 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <div id="label">Memory maps</div>
+ <div id="contents">
+ <div id="info_text">No memory maps selected</div>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const Scalar = tr.b.Scalar;
+ const sizeInBytes_smallerIsBetter =
+ tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
+
+ const CONSTANT_COLUMN_RULES = [
+ {
+ condition: 'Start address',
+ importance: 0,
+ columnConstructor: tr.ui.analysis.StringMemoryColumn
+ }
+ ];
+
+ const VARIABLE_COLUMN_RULES = [
+ {
+ condition: 'Virtual size',
+ importance: 7,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Protection flags',
+ importance: 6,
+ columnConstructor: tr.ui.analysis.StringMemoryColumn
+ },
+ {
+ condition: 'PSS',
+ importance: 5,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Private dirty',
+ importance: 4,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Private clean',
+ importance: 3,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Shared dirty',
+ importance: 2,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Shared clean',
+ importance: 1,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ },
+ {
+ condition: 'Swapped',
+ importance: 0,
+ columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
+ }
+ ];
+
+ const BYTE_STAT_COLUMN_MAP = {
+ 'proportionalResident': 'PSS',
+ 'privateDirtyResident': 'Private dirty',
+ 'privateCleanResident': 'Private clean',
+ 'sharedDirtyResident': 'Shared dirty',
+ 'sharedCleanResident': 'Shared clean',
+ 'swapped': 'Swapped'
+ };
+
+ function hexString(address, is64BitAddress) {
+ if (address === undefined) return undefined;
+ const hexPadding = is64BitAddress ? '0000000000000000' : '00000000';
+ return (hexPadding + address.toString(16)).substr(-hexPadding.length);
+ }
+
+ function pruneEmptyRuleRows(row) {
+ if (row.subRows === undefined || row.subRows.length === 0) return;
+
+ // Either all sub-rows are rule rows, or all sub-rows are VM region rows.
+ if (row.subRows[0].rule === undefined) {
+ // VM region rows: Early out to avoid filtering a large array for
+ // performance reasons (no sub-rows would be removed, but the whole array
+ // would be unnecessarily copied to a new array).
+ return;
+ }
+
+ row.subRows.forEach(pruneEmptyRuleRows);
+ row.subRows = row.subRows.filter(function(subRow) {
+ return subRow.subRows.length > 0;
+ });
+ }
+
+ Polymer({
+ is: 'tr-ui-a-memory-dump-vm-regions-details-pane',
+ behaviors: [tr.ui.analysis.StackedPane],
+
+ created() {
+ this.vmRegions_ = undefined;
+ this.aggregationMode_ = undefined;
+ },
+
+ ready() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ },
+
+ /**
+ * Sets the VM regions and schedules rebuilding the pane.
+ *
+ * The provided value should be a chronological list of lists of VM
+ * regions. All VM regions are assumed to belong to the same process.
+ * Example:
+ *
+ * [
+ * [
+ * // VM regions at timestamp 1.
+ * tr.model.VMRegion {},
+ * tr.model.VMRegion {},
+ * tr.model.VMRegion {}
+ * ],
+ * undefined, // No VM regions provided at timestamp 2.
+ * [
+ * // VM regions at timestamp 3.
+ * tr.model.VMRegion {},
+ * tr.model.VMRegion {}
+ * ]
+ * ]
+ */
+ set vmRegions(vmRegions) {
+ this.vmRegions_ = vmRegions;
+ this.scheduleRebuild_();
+ },
+
+ get vmRegions() {
+ return this.vmRegions_;
+ },
+
+ set aggregationMode(aggregationMode) {
+ this.aggregationMode_ = aggregationMode;
+ this.scheduleRebuild_();
+ },
+
+ get aggregationMode() {
+ return this.aggregationMode_;
+ },
+
+ onRebuild_() {
+ if (this.vmRegions_ === undefined || this.vmRegions_.length === 0) {
+ // Show the info text (hide the table).
+ this.$.info_text.style.display = 'block';
+ this.$.table.style.display = 'none';
+
+ this.$.table.clear();
+ this.$.table.rebuild();
+ return;
+ }
+
+ // Show the table (hide the info text).
+ this.$.info_text.style.display = 'none';
+ this.$.table.style.display = 'block';
+
+ const rows = this.createRows_(this.vmRegions_);
+ const columns = this.createColumns_(rows);
+
+ // Note: There is no need to aggregate fields of the VM regions because
+ // the classification tree already takes care of that.
+
+ this.$.table.tableRows = rows;
+ this.$.table.tableColumns = columns;
+
+ // TODO(petrcermak): This can be quite slow. Consider doing this somehow
+ // asynchronously.
+ this.$.table.rebuild();
+
+ tr.ui.analysis.expandTableRowsRecursively(this.$.table);
+ },
+
+ createRows_(timeToVmRegionTree) {
+ // Determine if any start address is outside the 32-bit range.
+ const is64BitAddress = timeToVmRegionTree.some(function(vmRegionTree) {
+ if (vmRegionTree === undefined) return false;
+ return vmRegionTree.someRegion(function(region) {
+ if (region.startAddress === undefined) return false;
+ return region.startAddress >= 4294967296; /* 2^32 */
+ });
+ });
+
+ return [
+ this.createClassificationNodeRow(timeToVmRegionTree, is64BitAddress)
+ ];
+ },
+
+ createClassificationNodeRow(timeToNode, is64BitAddress) {
+ // Get any defined classification node so that we can extract the
+ // properties which don't change over time.
+ const definedNode = timeToNode.find(x => x);
+
+ // Child node ID (list index) -> Timestamp (list index) ->
+ // VM region classification node.
+ const childNodeIdToTimeToNode = Object.values(
+ tr.b.invertArrayOfDicts(timeToNode, function(node) {
+ const children = node.children;
+ if (children === undefined) return undefined;
+ const childMap = {};
+ children.forEach(function(childNode) {
+ if (!childNode.hasRegions) return;
+ childMap[childNode.title] = childNode;
+ });
+ return childMap;
+ }));
+ const childNodeSubRows = childNodeIdToTimeToNode.map(
+ function(timeToChildNode) {
+ return this.createClassificationNodeRow(
+ timeToChildNode, is64BitAddress);
+ }, this);
+
+ // Region ID (list index) -> Timestamp (list index) -> VM region.
+ const regionIdToTimeToRegion = Object.values(
+ tr.b.invertArrayOfDicts(timeToNode, function(node) {
+ const regions = node.regions;
+ if (regions === undefined) return undefined;
+
+ const results = {};
+ for (const region of regions) {
+ results[region.uniqueIdWithinProcess] = region;
+ }
+ return results;
+ }));
+ const regionSubRows = regionIdToTimeToRegion.map(function(timeToRegion) {
+ return this.createRegionRow_(timeToRegion, is64BitAddress);
+ }, this);
+
+ const subRows = childNodeSubRows.concat(regionSubRows);
+
+ return {
+ title: definedNode.title,
+ contexts: timeToNode,
+ variableCells: this.createVariableCells_(timeToNode),
+ subRows
+ };
+ },
+
+ createRegionRow_(timeToRegion, is64BitAddress) {
+ // Get any defined VM region so that we can extract the properties which
+ // don't change over time.
+ const definedRegion = timeToRegion.find(x => x);
+
+ return {
+ title: definedRegion.mappedFile,
+ contexts: timeToRegion,
+ constantCells: this.createConstantCells_(definedRegion, is64BitAddress),
+ variableCells: this.createVariableCells_(timeToRegion)
+ };
+ },
+
+ /**
+ * Create cells for VM region properties which DON'T change over time.
+ *
+ * Note that there are currently no such properties of classification nodes.
+ */
+ createConstantCells_(definedRegion, is64BitAddress) {
+ return tr.ui.analysis.createCells([definedRegion], function(region) {
+ const startAddress = region.startAddress;
+ if (startAddress === undefined) return undefined;
+ return { 'Start address': hexString(startAddress, is64BitAddress) };
+ });
+ },
+
+ /**
+ * Create cells for VM region (classification node) properties which DO
+ * change over time.
+ */
+ createVariableCells_(timeToRegion) {
+ return tr.ui.analysis.createCells(timeToRegion, function(region) {
+ const fields = {};
+
+ const sizeInBytes = region.sizeInBytes;
+ if (sizeInBytes !== undefined) {
+ fields['Virtual size'] = new Scalar(
+ sizeInBytes_smallerIsBetter, sizeInBytes);
+ }
+ const protectionFlags = region.protectionFlagsToString;
+ if (protectionFlags !== undefined) {
+ fields['Protection flags'] = protectionFlags;
+ }
+
+ for (const [byteStatName, columnName] of
+ Object.entries(BYTE_STAT_COLUMN_MAP)) {
+ const byteStat = region.byteStats[byteStatName];
+ if (byteStat === undefined) continue;
+ fields[columnName] = new Scalar(
+ sizeInBytes_smallerIsBetter, byteStat);
+ }
+
+ return fields;
+ });
+ },
+
+ createColumns_(rows) {
+ const titleColumn = new tr.ui.analysis.TitleColumn('Mapped file');
+ titleColumn.width = '200px';
+
+ const constantColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'constantCells',
+ aggregationMode: undefined,
+ rules: CONSTANT_COLUMN_RULES
+ });
+ const variableColumns = tr.ui.analysis.MemoryColumn.fromRows(rows, {
+ cellKey: 'variableCells',
+ aggregationMode: this.aggregationMode_,
+ rules: VARIABLE_COLUMN_RULES
+ });
+ const fieldColumns = constantColumns.concat(variableColumns);
+ tr.ui.analysis.MemoryColumn.spaceEqually(fieldColumns);
+
+ const columns = [titleColumn].concat(fieldColumns);
+ return columns;
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html
new file mode 100644
index 00000000000..7534727091b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/memory_dump_vm_regions_details_pane_test.html
@@ -0,0 +1,496 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_sub_view_test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
+<link rel="import"
+ href="/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const TitleColumn = tr.ui.analysis.TitleColumn;
+ const StringMemoryColumn = tr.ui.analysis.StringMemoryColumn;
+ const NumericMemoryColumn = tr.ui.analysis.NumericMemoryColumn;
+ const AggregationMode = tr.ui.analysis.MemoryColumn.AggregationMode;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const checkSizeNumericFields = tr.ui.analysis.checkSizeNumericFields;
+ const checkStringFields = tr.ui.analysis.checkStringFields;
+ const checkColumns = tr.ui.analysis.checkColumns;
+ const isElementDisplayed = tr.ui.analysis.isElementDisplayed;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ function createVMRegions() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+
+ // First timestamp.
+ const gmd1 = addGlobalMemoryDump(
+ model, {ts: 42, levelOfDetail: DETAILED});
+ const pmd1 = addProcessMemoryDump(gmd1, process, {ts: 42});
+ pmd1.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ mappedFile: '/lib/chrome.so',
+ startAddress: 65536,
+ sizeInBytes: 536870912,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 8192
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0',
+ startAddress: 140296983150592,
+ sizeInBytes: 2097152,
+ protectionFlags: 0,
+ byteStats: {
+ proportionalResident: 0
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 10995116277760,
+ sizeInBytes: 2147483648,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ privateDirtyResident: 0,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 12094627905536,
+ sizeInBytes: 2147483648,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ privateDirtyResident: 0,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/dev/ashmem/dalvik-zygote space',
+ startAddress: 13194139533312,
+ sizeInBytes: 100,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 100,
+ privateDirtyResident: 0,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/dev/ashmem/libc malloc',
+ startAddress: 14293651161088,
+ sizeInBytes: 200,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 200,
+ privateDirtyResident: 96,
+ swapped: 0
+ }
+ })
+ ]);
+
+ // This is here so that we could test that tracing is discounted from the
+ // 'Native heap' category.
+ pmd1.memoryAllocatorDumps = [
+ newAllocatorDump(pmd1, 'tracing',
+ {numerics: {size: 500, resident_size: 32}})
+ ];
+
+ // Second timestamp.
+ const gmd2 = addGlobalMemoryDump(
+ model, {ts: 42, levelOfDetail: DETAILED});
+ const pmd2 = addProcessMemoryDump(gmd2, process, {ts: 42});
+ pmd2.vmRegions = VMRegionClassificationNode.fromRegions([
+ VMRegion.fromDict({
+ mappedFile: '/lib/chrome.so',
+ startAddress: 65536,
+ sizeInBytes: 536870912,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 9216
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/lib/chrome.so',
+ startAddress: 140296983150592,
+ sizeInBytes: 536870912,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 10240
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 10995116277760,
+ sizeInBytes: 2147483648,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ privateDirtyResident: 0,
+ swapped: 32
+ }
+ }),
+ VMRegion.fromDict({
+ startAddress: 12094627905536,
+ sizeInBytes: 2147483648,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_WRITE,
+ byteStats: {
+ privateDirtyResident: 0,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/dev/ashmem/dalvik-zygote space',
+ startAddress: 13194139533312,
+ sizeInBytes: 100,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 0,
+ privateDirtyResident: 100,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/dev/ashmem/libc malloc',
+ startAddress: 14293651161088,
+ sizeInBytes: 200,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ |
+ VMRegion.PROTECTION_FLAG_EXECUTE,
+ byteStats: {
+ proportionalResident: 100,
+ privateDirtyResident: 96,
+ swapped: 0
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: '/usr/share/fonts/DejaVuSansMono.ttf',
+ startAddress: 140121259503616,
+ sizeInBytes: 335872,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ byteStats: {
+ proportionalResident: 22528
+ }
+ }),
+ VMRegion.fromDict({
+ mappedFile: 'another-map',
+ startAddress: 52583094233905872,
+ sizeInBytes: 1,
+ byteStats: {
+ proportionalResident: 1,
+ privateDirtyResident: 1,
+ swapped: 1
+ }
+ })
+ ]);
+ });
+
+ return model.processes[1].memoryDumps.map(function(pmd) {
+ return pmd.mostRecentVmRegions;
+ });
+ }
+
+ const EXPECTED_COLUMNS = [
+ { title: 'Mapped file', type: TitleColumn, noAggregation: true },
+ { title: 'Start address', type: StringMemoryColumn, noAggregation: true },
+ { title: 'Virtual size', type: NumericMemoryColumn },
+ { title: 'Protection flags', type: StringMemoryColumn },
+ { title: 'PSS', type: NumericMemoryColumn },
+ { title: 'Private dirty', type: NumericMemoryColumn },
+ { title: 'Swapped', type: NumericMemoryColumn }
+ ];
+
+ function checkRow(columns, row, expectedTitle, expectedStartAddress,
+ expectedVirtualSize, expectedProtectionFlags,
+ expectedProportionalResidentValues, expectedPrivateDirtyResidentValues,
+ expectedSwappedValues, expectedSubRowCount, expectedContexts) {
+ assert.strictEqual(columns[0].formatTitle(row), expectedTitle);
+ checkStringFields(row, columns[1], expectedStartAddress);
+ checkSizeNumericFields(row, columns[2], expectedVirtualSize);
+ checkStringFields(row, columns[3], expectedProtectionFlags);
+ checkSizeNumericFields(row, columns[4], expectedProportionalResidentValues);
+ checkSizeNumericFields(row, columns[5], expectedPrivateDirtyResidentValues);
+ checkSizeNumericFields(row, columns[6], expectedSwappedValues);
+
+ if (expectedSubRowCount === undefined) {
+ assert.isUndefined(row.subRows);
+ } else {
+ assert.lengthOf(row.subRows, expectedSubRowCount);
+ }
+
+ if (typeof expectedContexts === 'function') {
+ expectedContexts(row.contexts);
+ } else if (expectedContexts !== undefined) {
+ assert.deepEqual(Array.from(row.contexts), expectedContexts);
+ } else {
+ assert.isUndefined(row.contexts);
+ }
+ }
+
+ function genericMatcher(callback, defined) {
+ return function(actualValues) {
+ assert.lengthOf(actualValues, defined.length);
+ for (let i = 0; i < defined.length; i++) {
+ const actualValue = actualValues[i];
+ if (defined[i]) {
+ callback(actualValue);
+ } else {
+ assert.isUndefined(actualValue);
+ }
+ }
+ };
+ }
+
+ function vmRegionsMatcher(expectedMappedFile, expectedStartAddress, defined) {
+ return genericMatcher(function(actualRegion) {
+ assert.instanceOf(actualRegion, VMRegion);
+ assert.strictEqual(actualRegion.mappedFile, expectedMappedFile);
+ assert.strictEqual(actualRegion.startAddress, expectedStartAddress);
+ }, defined);
+ }
+
+ function classificationNodesMatcher(expectedTitle, defined) {
+ return genericMatcher(function(actualNode) {
+ assert.instanceOf(actualNode, VMRegionClassificationNode);
+ assert.strictEqual(actualNode.title, expectedTitle);
+ }, defined);
+ }
+
+ test('instantiate_empty', function() {
+ tr.ui.analysis.createAndCheckEmptyPanes(this,
+ 'tr-ui-a-memory-dump-vm-regions-details-pane', 'vmRegions',
+ function(viewEl) {
+ // Check that the info text is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.info_text));
+ assert.isFalse(isElementDisplayed(viewEl.$.table));
+ });
+ });
+
+ test('instantiate_single', function() {
+ const vmRegions = createVMRegions().slice(0, 1);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ viewEl.vmRegions = vmRegions;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, undefined /* no aggregation */);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check the rows of the table.
+ const totalRow = rows[0];
+ checkRow(columns, totalRow, 'Total', undefined, [4833935160], undefined,
+ [8460], [64], [0], 3, vmRegions);
+
+ const androidRow = totalRow.subRows[0];
+ checkRow(columns, androidRow, 'Android', undefined, [100], undefined,
+ [100], [0], [0], 1, classificationNodesMatcher('Android', [true]));
+
+ const javaRuntimeRow = androidRow.subRows[0];
+ checkRow(columns, javaRuntimeRow, 'Java runtime', undefined, [100],
+ undefined, [100], [0], [0], 1,
+ classificationNodesMatcher('Java runtime', [true]));
+
+ const spacesRow = javaRuntimeRow.subRows[0];
+ checkRow(columns, spacesRow, 'Spaces', undefined, [100], undefined, [100],
+ [0], [0], 1, classificationNodesMatcher('Spaces', [true]));
+
+ const nativeHeapRow = totalRow.subRows[1];
+ checkRow(columns, nativeHeapRow, 'Native heap', undefined, [4294966996],
+ undefined, [168], [64], [0], 4,
+ classificationNodesMatcher('Native heap', [true]));
+
+ const discountedTracingOverheadRow = nativeHeapRow.subRows[3];
+ checkRow(columns, discountedTracingOverheadRow,
+ '[discounted tracing overhead]', undefined, [-500], undefined, [-32],
+ [-32], undefined, undefined,
+ vmRegionsMatcher('[discounted tracing overhead]', undefined, [true]));
+
+ const filesRow = totalRow.subRows[2];
+ checkRow(columns, filesRow, 'Files', undefined, [538968064], undefined,
+ [8192], undefined, undefined, 1,
+ classificationNodesMatcher('Files', [true]));
+
+ const soRow = filesRow.subRows[0];
+ checkRow(columns, soRow, 'so', undefined, [538968064], undefined,
+ [8192], undefined, undefined, 2,
+ classificationNodesMatcher('so', [true]));
+
+ const mmapChromeRow = soRow.subRows[0];
+ checkRow(columns, mmapChromeRow, '/lib/chrome.so', ['0000000000010000'],
+ [536870912], ['r-xp'], [8192], undefined, undefined, undefined,
+ vmRegionsMatcher('/lib/chrome.so', 65536, [true]));
+
+ const mmapLibX11Row = soRow.subRows[1];
+ checkRow(columns, mmapLibX11Row,
+ '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', ['00007f996fd80000'],
+ [2097152], ['---p'], [0], undefined, undefined, undefined,
+ vmRegionsMatcher('/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0',
+ 140296983150592, [true]));
+ });
+
+ test('instantiate_multipleDiff', function() {
+ const vmRegions = createVMRegions();
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ viewEl.vmRegions = vmRegions;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+
+ // Check the rows of the table.
+ const totalRow = rows[0];
+ checkRow(columns, totalRow, 'Total', undefined, [4833935160, 5369045293],
+ undefined, [8460, 42085], [64, 197], [0, 33], 4, vmRegions);
+
+ const androidRow = totalRow.subRows[0];
+ checkRow(columns, androidRow, 'Android', undefined, [100, 100], undefined,
+ [100, 0], [0, 100], [0, 0], 1,
+ classificationNodesMatcher('Android', [true, true]));
+
+ const javaRuntimeRow = androidRow.subRows[0];
+ checkRow(columns, javaRuntimeRow, 'Java runtime', undefined, [100, 100],
+ undefined, [100, 0], [0, 100], [0, 0], 1,
+ classificationNodesMatcher('Java runtime', [true, true]));
+
+ const spacesRow = javaRuntimeRow.subRows[0];
+ checkRow(columns, spacesRow, 'Spaces', undefined, [100, 100], undefined,
+ [100, 0], [0, 100], [0, 0], 1,
+ classificationNodesMatcher('Spaces', [true, true]));
+
+ const nativeHeapRow = totalRow.subRows[1];
+ checkRow(columns, nativeHeapRow, 'Native heap', undefined,
+ [4294966996, 4294967496], undefined, [168, 100], [64, 96], [0, 32], 4,
+ classificationNodesMatcher('Native heap', [true, true]));
+
+ const discountedTracingOverheadRow = nativeHeapRow.subRows[3];
+ checkRow(columns, discountedTracingOverheadRow,
+ '[discounted tracing overhead]', undefined, [-500, undefined],
+ undefined, [-32, undefined], [-32, undefined], undefined, undefined,
+ vmRegionsMatcher('[discounted tracing overhead]', undefined,
+ [true, false]));
+
+ const filesRow = totalRow.subRows[2];
+ checkRow(columns, filesRow, 'Files', undefined, [538968064, 1074077696],
+ undefined, [8192, 41984], undefined, undefined, 2,
+ classificationNodesMatcher('Files', [true, true]));
+
+ const soRow = filesRow.subRows[0];
+ checkRow(columns, soRow, 'so', undefined, [538968064, 1073741824],
+ undefined, [8192, 19456], undefined, undefined, 3,
+ classificationNodesMatcher('so', [true, true]));
+
+ const mmapChromeRow = soRow.subRows[0];
+ checkRow(columns, mmapChromeRow, '/lib/chrome.so', ['0000000000010000'],
+ [536870912, 536870912], ['r-xp', 'r-xp'], [8192, 9216], undefined,
+ undefined, undefined,
+ vmRegionsMatcher('/lib/chrome.so', 65536, [true, true]));
+
+ const mmapLibX11Row = soRow.subRows[1];
+ checkRow(columns, mmapLibX11Row,
+ '/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0', ['00007f996fd80000'],
+ [2097152, undefined], ['---p', undefined], [0, undefined], undefined,
+ undefined, undefined,
+ vmRegionsMatcher('/usr/lib/x86_64-linux-gnu/libX11.so.6.3.0',
+ 140296983150592, [true, false]));
+
+ const otherRow = totalRow.subRows[3];
+ checkRow(columns, otherRow, 'Other', undefined, [undefined, 1], undefined,
+ [undefined, 1], [undefined, 1], [undefined, 1], 1,
+ classificationNodesMatcher('Other', [false, true]));
+
+ const anotherMapRow = otherRow.subRows[0];
+ checkRow(columns, anotherMapRow, 'another-map', ['00bad00bad00bad0'],
+ [undefined, 1], undefined, [undefined, 1], [undefined, 1],
+ [undefined, 1], undefined,
+ vmRegionsMatcher('another-map', 52583094233905872, [false, true]));
+ });
+
+ test('instantiate_multipleMax', function() {
+ const vmRegions = createVMRegions();
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ viewEl.vmRegions = vmRegions;
+ viewEl.aggregationMode = AggregationMode.MAX;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ // Just check that the aggregation mode was propagated to the columns.
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.MAX);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+ });
+
+ test('instantiate_multipleWithUndefined', function() {
+ const vmRegions = createVMRegions();
+ vmRegions.splice(1, 0, undefined);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-memory-dump-vm-regions-details-pane');
+ viewEl.vmRegions = vmRegions;
+ viewEl.aggregationMode = AggregationMode.DIFF;
+ viewEl.rebuild();
+ this.addHTMLOutput(viewEl);
+
+ // Check that the table is shown.
+ assert.isTrue(isElementDisplayed(viewEl.$.table));
+ assert.isFalse(isElementDisplayed(viewEl.$.info_text));
+
+ // Just check that the table has the right shape.
+ const table = viewEl.$.table;
+ const columns = table.tableColumns;
+ checkColumns(columns, EXPECTED_COLUMNS, AggregationMode.DIFF);
+ const rows = table.tableRows;
+ assert.lengthOf(rows, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html
new file mode 100644
index 00000000000..0bb39b7e0f2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+
+<dom-module id='tr-ui-a-multi-async-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #container {
+ display: flex;
+ flex: 1 1 auto;
+ }
+ #events {
+ margin-left: 8px;
+ flex: 0 1 200px;
+ }
+ </style>
+ <div id="container">
+ <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view>
+ <div id="events">
+ <tr-ui-a-related-events id="relatedEvents"></tr-ui-a-related-events>
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-async-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.selection = selection;
+ this.$.relatedEvents.setRelatedEvents(selection);
+ if (this.$.relatedEvents.hasRelatedEvents()) {
+ this.$.relatedEvents.style.display = '';
+ } else {
+ this.$.relatedEvents.style.display = 'none';
+ }
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.$.content.selection) return undefined;
+
+ const selection = new tr.model.EventSet();
+ this.$.content.selection.forEach(function(asyncEvent) {
+ if (!asyncEvent.associatedEvents) return;
+
+ asyncEvent.associatedEvents.forEach(function(event) {
+ selection.push(event);
+ });
+ });
+ if (selection.length) return selection;
+ return undefined;
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-async-slice-sub-view',
+ tr.model.AsyncSlice,
+ {
+ multi: true,
+ title: 'Async Slices',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html
new file mode 100644
index 00000000000..20fb52c058f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_async_slice_sub_view_test.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_async_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx;
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.asyncSliceGroup.push(newAsyncSliceEx({
+ title: 'a',
+ start: 10,
+ end: 20,
+ startThread: t1,
+ endThread: t1
+ }));
+ t1.asyncSliceGroup.push(newAsyncSliceEx({
+ title: 'b',
+ start: 25,
+ end: 40,
+ startThread: t1,
+ endThread: t1
+ }));
+
+ const selection = new tr.model.EventSet();
+ selection.push(t1.asyncSliceGroup.slices[0]);
+ selection.push(t1.asyncSliceGroup.slices[1]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-async-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html
new file mode 100644
index 00000000000..4525df0e8c2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<dom-module id='tr-ui-a-multi-cpu-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #content {
+ flex: 1 1 auto;
+ }
+ </style>
+ <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-cpu-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.$.content.eventsHaveSubRows = false;
+ },
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.setSelectionWithoutErrorChecks(selection);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-cpu-slice-sub-view',
+ tr.model.CpuSlice,
+ {
+ multi: true,
+ title: 'CPU Slices',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html
new file mode 100644
index 00000000000..36dc99bd338
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_cpu_slice_sub_view_test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_cpu_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBasicModel() {
+ const lines = [
+ 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('instantiate', function() {
+ const m = createBasicModel();
+ const cpu = m.kernel.cpus[1];
+ assert.isDefined(cpu);
+
+ const selection = new tr.model.EventSet();
+ selection.push(cpu.slices[0]);
+ selection.push(cpu.slices[1]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-cpu-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html
new file mode 100644
index 00000000000..52908c620bc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view.html
@@ -0,0 +1,211 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_summary_table.html">
+<link rel="import" href="/tracing/ui/analysis/selection_summary_table.html">
+<link rel="import" href="/tracing/ui/base/radio_picker.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/value/diagnostics/scalar.html">
+<link rel="import" href="/tracing/value/histogram.html">
+<link rel="import" href="/tracing/value/ui/histogram_span.html">
+
+<dom-module id='tr-ui-a-multi-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ overflow: auto;
+ }
+ #content {
+ display: flex;
+ flex-direction: column;
+ flex: 0 1 auto;
+ align-self: stretch;
+ }
+ #content > * {
+ flex: 0 0 auto;
+ align-self: stretch;
+ }
+ #histogramContainer {
+ display: flex;
+ }
+
+ tr-ui-a-multi-event-summary-table {
+ border-bottom: 1px solid #aaa;
+ }
+
+ tr-ui-a-selection-summary-table {
+ margin-top: 1.25em;
+ border-top: 1px solid #aaa;
+ background-color: #eee;
+ font-weight: bold;
+ margin-bottom: 1.25em;
+ border-bottom: 1px solid #aaa;
+ }
+ </style>
+ <div id="content">
+ <tr-ui-a-multi-event-summary-table id="eventSummaryTable">
+ </tr-ui-a-multi-event-summary-table>
+ <tr-ui-a-selection-summary-table id="selectionSummaryTable">
+ </tr-ui-a-selection-summary-table>
+ <tr-ui-b-radio-picker id="radioPicker">
+ </tr-ui-b-radio-picker>
+ <div id="histogramContainer">
+ <tr-v-ui-histogram-span id="histogramSpan">
+ </tr-v-ui-histogram-span>
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const EVENT_FIELD = [
+ {key: 'start', label: 'Start'},
+ {key: 'cpuDuration', label: 'CPU Duration'},
+ {key: 'duration', label: 'Duration'},
+ {key: 'cpuSelfTime', label: 'CPU Self Time'},
+ {key: 'selfTime', label: 'Self Time'}
+ ];
+
+ function buildDiagnostics_(slice) {
+ const diagnostics = {};
+ for (const item of EVENT_FIELD) {
+ const fieldName = item.key;
+ if (slice[fieldName] === undefined) continue;
+ diagnostics[fieldName] = new tr.v.d.Scalar(new tr.b.Scalar(
+ tr.b.Unit.byName.timeDurationInMs, slice[fieldName]));
+ }
+ diagnostics.args = new tr.v.d.GenericSet([slice.args]);
+ diagnostics.event = new tr.v.d.RelatedEventSet(slice);
+ return diagnostics;
+ }
+
+ Polymer({
+ is: 'tr-ui-a-multi-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ this.eventsHaveDuration_ = true;
+ this.eventsHaveSubRows_ = true;
+ },
+
+ ready() {
+ this.$.radioPicker.style.display = 'none';
+ this.$.radioPicker.items = EVENT_FIELD;
+ this.$.radioPicker.select('cpuSelfTime');
+ this.$.radioPicker.addEventListener('change', () => {
+ if (this.isAttached) this.updateContents_();
+ });
+
+ this.$.histogramSpan.graphWidth = 400;
+ this.$.histogramSpan.canMergeSampleDiagnostics = false;
+ this.$.histogramContainer.style.display = 'none';
+ },
+
+ attached() {
+ if (this.currentSelection_ !== undefined) this.updateContents_();
+ },
+
+ set selection(selection) {
+ if (selection.length <= 1) {
+ throw new Error('Only supports multiple items');
+ }
+ this.setSelectionWithoutErrorChecks(selection);
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ setSelectionWithoutErrorChecks(selection) {
+ this.currentSelection_ = selection;
+ if (this.isAttached) this.updateContents_();
+ },
+
+ get eventsHaveDuration() {
+ return this.eventsHaveDuration_;
+ },
+
+ set eventsHaveDuration(eventsHaveDuration) {
+ this.eventsHaveDuration_ = eventsHaveDuration;
+ if (this.isAttached) this.updateContents_();
+ },
+
+ get eventsHaveSubRows() {
+ return this.eventsHaveSubRows_;
+ },
+
+ set eventsHaveSubRows(eventsHaveSubRows) {
+ this.eventsHaveSubRows_ = eventsHaveSubRows;
+ if (this.isAttached) this.updateContents_();
+ },
+
+ buildHistogram_(selectedKey) {
+ let leftBoundary = Number.MAX_VALUE;
+ let rightBoundary = tr.b.math.Statistics.percentile(
+ this.currentSelection_, 0.95,
+ function(value) {
+ leftBoundary = Math.min(leftBoundary, value[selectedKey]);
+ return value[selectedKey];
+ });
+
+ if (leftBoundary === rightBoundary) rightBoundary += 1;
+ const histogram = new tr.v.Histogram(
+ '',
+ tr.b.Unit.byName.timeDurationInMs,
+ tr.v.HistogramBinBoundaries.createLinear(
+ leftBoundary, rightBoundary,
+ Math.ceil(Math.sqrt(this.currentSelection_.length))));
+ histogram.customizeSummaryOptions({sum: false});
+ for (const slice of this.currentSelection_) {
+ histogram.addSample(slice[selectedKey],
+ buildDiagnostics_(slice));
+ }
+
+ return histogram;
+ },
+
+ updateContents_() {
+ const selection = this.currentSelection_;
+ if (!selection) return;
+
+ const eventsByTitle = selection.getEventsOrganizedByTitle();
+ const numTitles = Object.keys(eventsByTitle).length;
+
+ this.$.eventSummaryTable.configure({
+ showTotals: numTitles > 1,
+ eventsByTitle,
+ eventsHaveDuration: this.eventsHaveDuration_,
+ eventsHaveSubRows: this.eventsHaveSubRows_
+ });
+
+ this.$.selectionSummaryTable.selection = this.currentSelection_;
+
+ if (numTitles === 1) {
+ this.$.radioPicker.style.display = 'block';
+ this.$.histogramContainer.style.display = 'flex';
+ this.$.histogramSpan.build(
+ this.buildHistogram_(this.$.radioPicker.selectedKey));
+ if (this.$.histogramSpan.histogram.numValues === 0) {
+ this.$.histogramContainer.style.display = 'none';
+ }
+ } else {
+ this.$.radioPicker.style.display = 'none';
+ this.$.histogramContainer.style.display = 'none';
+ }
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html
new file mode 100644
index 00000000000..9958b7db81c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_sub_view_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const Thread = tr.model.Thread;
+ const EventSet = tr.model.EventSet;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('differentTitles', function() {
+ const model = new Model();
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'a', start: 0.0, duration: 0.04}));
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'a', start: 0.12, duration: 0.06}));
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'aa', start: 0.5, duration: 0.5}));
+ t53.sliceGroup.createSubSlices();
+
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+ selection.push(t53.sliceGroup.slices[1]);
+ selection.push(t53.sliceGroup.slices[2]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+
+ const summaryTableEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-multi-event-summary-table');
+ assert.isTrue(summaryTableEl.showTotals);
+ assert.lengthOf(Object.keys(summaryTableEl.eventsByTitle), 2);
+
+ const selectionSummaryTableEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-selection-summary-table');
+ assert.strictEqual(selectionSummaryTableEl.selection, selection);
+
+ const radioPickerEl =
+ tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-radio-picker');
+ assert.strictEqual(radioPickerEl.style.display, 'none');
+ });
+
+ test('sameTitles', function() {
+ const model = new Model();
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'c', start: 0.0, duration: 0.04}));
+ t53.sliceGroup.pushSlice(newSliceEx(
+ {title: 'c', start: 0.12, duration: 0.06}));
+ t53.sliceGroup.createSubSlices();
+
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+ selection.push(t53.sliceGroup.slices[1]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+
+ const summaryTableEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-multi-event-summary-table');
+ assert.isFalse(summaryTableEl.showTotals);
+ assert.lengthOf(Object.keys(summaryTableEl.eventsByTitle), 1);
+
+ const selectionSummaryTableEl = tr.ui.b.findDeepElementMatching(
+ viewEl, 'tr-ui-a-selection-summary-table');
+ assert.strictEqual(selectionSummaryTableEl.selection, selection);
+
+ const radioPickerEl =
+ tr.ui.b.findDeepElementMatching(viewEl, 'tr-ui-b-radio-picker');
+ assert.strictEqual(radioPickerEl.style.display, 'block');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html
new file mode 100644
index 00000000000..886e315863e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.ui.analysis', function() {
+ function MultiEventSummary(title, events) {
+ this.title = title;
+ this.duration_ = undefined;
+ this.selfTime_ = undefined;
+ this.events_ = events;
+
+ this.cpuTimesComputed_ = false;
+ this.cpuSelfTime_ = undefined;
+ this.cpuDuration_ = undefined;
+
+ this.maxDuration_ = undefined;
+ this.maxCpuDuration_ = undefined;
+ this.maxSelfTime_ = undefined;
+ this.maxCpuSelfTime_ = undefined;
+
+ this.untotallableArgs_ = [];
+ this.totalledArgs_ = undefined;
+ }
+ MultiEventSummary.prototype = {
+
+ set title(title) {
+ if (title === 'Totals') {
+ this.totalsRow = true;
+ }
+ this.title_ = title;
+ },
+
+ get title() {
+ return this.title_;
+ },
+
+ get duration() {
+ if (this.duration_ === undefined) {
+ this.duration_ = tr.b.math.Statistics.sum(
+ this.events_, function(event) {
+ return event.duration;
+ });
+ }
+ return this.duration_;
+ },
+
+ get cpuSelfTime() {
+ this.computeCpuTimesIfNeeded_();
+ return this.cpuSelfTime_;
+ },
+
+ get cpuDuration() {
+ this.computeCpuTimesIfNeeded_();
+ return this.cpuDuration_;
+ },
+
+ computeCpuTimesIfNeeded_() {
+ if (this.cpuTimesComputed_) return;
+ this.cpuTimesComputed_ = true;
+
+ let cpuSelfTime = 0;
+ let cpuDuration = 0;
+ let hasCpuData = false;
+ for (const event of this.events_) {
+ if (event.cpuDuration !== undefined) {
+ cpuDuration += event.cpuDuration;
+ hasCpuData = true;
+ }
+
+ if (event.cpuSelfTime !== undefined) {
+ cpuSelfTime += event.cpuSelfTime;
+ hasCpuData = true;
+ }
+ }
+ if (hasCpuData) {
+ this.cpuDuration_ = cpuDuration;
+ this.cpuSelfTime_ = cpuSelfTime;
+ }
+ },
+
+ get selfTime() {
+ if (this.selfTime_ === undefined) {
+ this.selfTime_ = 0;
+ for (const event of this.events_) {
+ if (event.selfTime !== undefined) {
+ this.selfTime_ += event.selfTime;
+ }
+ }
+ }
+ return this.selfTime_;
+ },
+
+ get events() {
+ return this.events_;
+ },
+
+ get numEvents() {
+ return this.events_.length;
+ },
+
+ get numAlerts() {
+ if (this.numAlerts_ === undefined) {
+ this.numAlerts_ = tr.b.math.Statistics.sum(this.events_, event =>
+ event.associatedAlerts.length
+ );
+ }
+ return this.numAlerts_;
+ },
+
+ get untotallableArgs() {
+ this.updateArgsIfNeeded_();
+ return this.untotallableArgs_;
+ },
+
+ get totalledArgs() {
+ this.updateArgsIfNeeded_();
+ return this.totalledArgs_;
+ },
+
+
+ get maxDuration() {
+ if (this.maxDuration_ === undefined) {
+ this.maxDuration_ = tr.b.math.Statistics.max(
+ this.events_, function(event) {
+ return event.duration;
+ });
+ }
+ return this.maxDuration_;
+ },
+
+
+ get maxCpuDuration() {
+ if (this.maxCpuDuration_ === undefined) {
+ this.maxCpuDuration_ = tr.b.math.Statistics.max(
+ this.events_, function(event) {
+ return event.cpuDuration;
+ });
+ }
+ return this.maxCpuDuration_;
+ },
+
+
+ get maxSelfTime() {
+ if (this.maxSelfTime_ === undefined) {
+ this.maxSelfTime_ = tr.b.math.Statistics.max(
+ this.events_, function(event) {
+ return event.selfTime;
+ });
+ }
+ return this.maxSelfTime_;
+ },
+
+
+ get maxCpuSelfTime() {
+ if (this.maxCpuSelfTime_ === undefined) {
+ this.maxCpuSelfTime_ = tr.b.math.Statistics.max(
+ this.events_, function(event) {
+ return event.cpuSelfTime;
+ });
+ }
+ return this.maxCpuSelfTime_;
+ },
+
+
+ updateArgsIfNeeded_() {
+ if (this.totalledArgs_ !== undefined) return;
+
+ const untotallableArgs = {};
+ const totalledArgs = {};
+ for (const event of this.events_) {
+ for (const argName in event.args) {
+ const argVal = event.args[argName];
+ const type = typeof argVal;
+ if (type !== 'number') {
+ untotallableArgs[argName] = true;
+ delete totalledArgs[argName];
+ continue;
+ }
+ if (untotallableArgs[argName]) {
+ continue;
+ }
+
+ if (totalledArgs[argName] === undefined) {
+ totalledArgs[argName] = 0;
+ }
+ totalledArgs[argName] += argVal;
+ }
+ }
+ this.untotallableArgs_ = Object.keys(untotallableArgs);
+ this.totalledArgs_ = totalledArgs;
+ }
+ };
+
+ return {
+ MultiEventSummary,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html
new file mode 100644
index 00000000000..1b32d606f61
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table.html
@@ -0,0 +1,358 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_summary.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-multi-event-summary-table'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-event-summary-table',
+
+ ready() {
+ this.showTotals_ = false;
+ this.eventsHaveDuration_ = true;
+ this.eventsHaveSubRows_ = true;
+ this.eventsByTitle_ = undefined;
+ },
+
+ updateTableColumns_(rows, maxValues) {
+ let hasCpuData = false;
+ let hasAlerts = false;
+ rows.forEach(function(row) {
+ if (row.cpuDuration !== undefined) {
+ hasCpuData = true;
+ }
+ if (row.cpuSelfTime !== undefined) {
+ hasCpuData = true;
+ }
+ if (row.numAlerts) {
+ hasAlerts = true;
+ }
+ });
+
+ const ownerDocument = this.ownerDocument;
+
+ const columns = [];
+
+ columns.push({
+ title: 'Name',
+ value(row) {
+ if (row.title === 'Totals') return 'Totals';
+ const container = document.createElement('div');
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(function() {
+ return new tr.model.EventSet(row.events);
+ }, row.title);
+ container.appendChild(linkEl);
+
+ if (tr.isExported('tr-ui-e-chrome-codesearch')) {
+ const link = document.createElement('tr-ui-e-chrome-codesearch');
+ link.searchPhrase = row.title;
+ container.appendChild(link);
+ }
+ return container;
+ },
+ width: '350px',
+ cmp(rowA, rowB) {
+ return rowA.title.localeCompare(rowB.title);
+ }
+ });
+ if (this.eventsHaveDuration_) {
+ columns.push({
+ title: 'Wall Duration',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.duration, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.duration),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.duration - rowB.duration;
+ }
+ });
+ }
+
+ if (this.eventsHaveDuration_ && hasCpuData) {
+ columns.push({
+ title: 'CPU Duration',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.cpuDuration, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.cpuDuration),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.cpuDuration - rowB.cpuDuration;
+ }
+ });
+ }
+
+ if (this.eventsHaveSubRows_ && this.eventsHaveDuration_) {
+ columns.push({
+ title: 'Self time',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.selfTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.selfTime),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.selfTime - rowB.selfTime;
+ }
+ });
+ }
+
+ if (this.eventsHaveSubRows_ && this.eventsHaveDuration_ && hasCpuData) {
+ columns.push({
+ title: 'CPU Self Time',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.cpuSelfTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.cpuSelfTime),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.cpuSelfTime - rowB.cpuSelfTime;
+ }
+ });
+ }
+
+ if (this.eventsHaveDuration_) {
+ columns.push({
+ title: 'Average ' + (hasCpuData ? 'CPU' : 'Wall') + ' Duration',
+ value(row) {
+ const totalDuration = hasCpuData ? row.cpuDuration : row.duration;
+ return tr.v.ui.createScalarSpan(totalDuration / row.numEvents, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ customContextRange: row.totalsRow ? undefined :
+ tr.b.math.Range.fromExplicitRange(0, maxValues.duration),
+ ownerDocument,
+ });
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ if (hasCpuData) {
+ return rowA.cpuDuration / rowA.numEvents -
+ rowB.cpuDuration / rowB.numEvents;
+ }
+ return rowA.duration / rowA.numEvents -
+ rowB.duration / rowB.numEvents;
+ }
+ });
+ }
+
+ columns.push({
+ title: 'Occurrences',
+ value(row) {
+ return row.numEvents;
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.numEvents - rowB.numEvents;
+ }
+ });
+
+ let alertsColumnIndex;
+ if (hasAlerts) {
+ columns.push({
+ title: 'Num Alerts',
+ value(row) {
+ return row.numAlerts;
+ },
+ width: '<upated further down>',
+ cmp(rowA, rowB) {
+ return rowA.numAlerts - rowB.numAlerts;
+ }
+ });
+ alertsColumnIndex = columns.length - 1;
+ }
+ let colWidthPercentage;
+ if (columns.length === 1) {
+ colWidthPercentage = '100%';
+ } else {
+ colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%';
+ }
+
+ for (let i = 1; i < columns.length; i++) {
+ columns[i].width = colWidthPercentage;
+ }
+
+ this.$.table.tableColumns = columns;
+
+ if (hasAlerts) {
+ this.$.table.sortColumnIndex = alertsColumnIndex;
+ this.$.table.sortDescending = true;
+ }
+ },
+
+ configure(config) {
+ if (config.eventsByTitle === undefined) {
+ throw new Error('Required: eventsByTitle');
+ }
+
+ if (config.showTotals !== undefined) {
+ this.showTotals_ = config.showTotals;
+ } else {
+ this.showTotals_ = true;
+ }
+
+ if (config.eventsHaveDuration !== undefined) {
+ this.eventsHaveDuration_ = config.eventsHaveDuration;
+ } else {
+ this.eventsHaveDuration_ = true;
+ }
+
+ if (config.eventsHaveSubRows !== undefined) {
+ this.eventsHaveSubRows_ = config.eventsHaveSubRows;
+ } else {
+ this.eventsHaveSubRows_ = true;
+ }
+
+ this.eventsByTitle_ = config.eventsByTitle;
+ this.updateContents_();
+ },
+
+ get showTotals() {
+ return this.showTotals_;
+ },
+
+ set showTotals(showTotals) {
+ this.showTotals_ = showTotals;
+ this.updateContents_();
+ },
+
+ get eventsHaveDuration() {
+ return this.eventsHaveDuration_;
+ },
+
+ set eventsHaveDuration(eventsHaveDuration) {
+ this.eventsHaveDuration_ = eventsHaveDuration;
+ this.updateContents_();
+ },
+
+ get eventsHaveSubRows() {
+ return this.eventsHaveSubRows_;
+ },
+
+ set eventsHaveSubRows(eventsHaveSubRows) {
+ this.eventsHaveSubRows_ = eventsHaveSubRows;
+ this.updateContents_();
+ },
+
+ get eventsByTitle() {
+ return this.eventsByTitle_;
+ },
+
+ set eventsByTitle(eventsByTitle) {
+ this.eventsByTitle_ = eventsByTitle;
+ this.updateContents_();
+ },
+
+ get selectionBounds() {
+ return this.selectionBounds_;
+ },
+
+ set selectionBounds(selectionBounds) {
+ this.selectionBounds_ = selectionBounds;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ let eventsByTitle;
+ if (this.eventsByTitle_ !== undefined) {
+ eventsByTitle = this.eventsByTitle_;
+ } else {
+ eventsByTitle = [];
+ }
+
+ const allEvents = new tr.model.EventSet();
+ const rows = [];
+ for (const [title, eventsOfSingleTitle] of Object.entries(eventsByTitle)) {
+ for (const event of eventsOfSingleTitle) allEvents.push(event);
+ const row = new tr.ui.analysis.MultiEventSummary(
+ title, eventsOfSingleTitle);
+ rows.push(row);
+ }
+
+ this.updateTableColumns_(rows);
+ this.$.table.tableRows = rows;
+
+ const maxValues = {
+ duration: undefined,
+ selfTime: undefined,
+ cpuSelfTime: undefined,
+ cpuDuration: undefined
+ };
+
+ if (this.eventsHaveDuration) {
+ for (const column in maxValues) {
+ maxValues[column] = tr.b.math.Statistics.max(rows, function(event) {
+ return event[column];
+ });
+ }
+ }
+
+ const footerRows = [];
+
+ if (this.showTotals_) {
+ const multiEventSummary = new tr.ui.analysis.MultiEventSummary(
+ 'Totals', allEvents);
+ footerRows.push(multiEventSummary);
+ }
+
+
+ this.updateTableColumns_(rows, maxValues);
+ this.$.table.tableRows = rows;
+
+ // TODO(selection bounds).
+
+ // TODO(sorting)
+
+ this.$.table.footerRows = footerRows;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html
new file mode 100644
index 00000000000..32efc0de1ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_table_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_summary_table.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('basicNoCpu', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, duration: 0.5}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0.5}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0.5}));
+ tsg.createSubSlices();
+
+ const threadTrack = {};
+ threadTrack.thread = thread;
+
+ const selection = new EventSet(tsg.slices);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-summary-table');
+ viewEl.configure({
+ showTotals: true,
+ eventsHaveDuration: true,
+ eventsByTitle: selection.getEventsOrganizedByTitle()
+ });
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('basicWithCpu', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+ cpuStart: 0, cpuEnd: 3}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+ cpuStart: 1, cpuEnd: 1.75}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+ cpuStart: 3, cpuEnd: 3.75}));
+ tsg.createSubSlices();
+
+ const threadTrack = {};
+ threadTrack.thread = thread;
+
+ const selection = new EventSet(tsg.slices);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-summary-table');
+ viewEl.configure({
+ showTotals: true,
+ eventsHaveDuration: true,
+ eventsByTitle: selection.getEventsOrganizedByTitle()
+ });
+ this.addHTMLOutput(viewEl);
+
+ const totals = tr.ui.b.findDeepElementMatchingPredicate(
+ viewEl, e => e.tagName === 'TFOOT');
+ const scalars = tr.ui.b.findDeepElementsMatchingPredicate(
+ totals, e => e.tagName === 'TR-V-UI-SCALAR-SPAN');
+ assert.strictEqual(scalars[0].value, 5);
+ assert.closeTo(scalars[1].value, 4.5, 1e-6);
+ assert.strictEqual(scalars[2].value, 4);
+ assert.closeTo(scalars[3].value, 3.75, 1e-6);
+ assert.closeTo(scalars[4].value, 1.5, 1e-6);
+ assert.strictEqual('3', totals.children[0].children[6].textContent);
+ });
+
+ test('noSelfTimeNoSubRows', function() {
+ const model = new Model();
+
+ const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+ const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {});
+
+ // Make reading some properties an explosion, as a way to ensure that they
+ // aren't read. Note that 'duration' is read since it is used by the
+ // EventSet to get the range.
+ const failProp = {
+ get() {
+ throw new Error('Should not be called');
+ }
+ };
+ Object.defineProperty(fe1, 'subRows', failProp);
+ Object.defineProperty(fe2, 'subRows', failProp);
+
+ Object.defineProperty(fe1, 'selfTime', failProp);
+ Object.defineProperty(fe2, 'selfTime', failProp);
+
+ model.flowEvents.push(fe1);
+ model.flowEvents.push(fe2);
+
+ const selection = new EventSet([fe1, fe2]);
+
+ const viewEl = document.createElement('tr-ui-a-multi-event-summary-table');
+ viewEl.configure({
+ showTotals: true,
+ eventsHaveDuration: false,
+ eventsHaveSubRows: false,
+ eventsByTitle: selection.getEventsOrganizedByTitle()
+ });
+ this.addHTMLOutput(viewEl);
+ });
+
+ // TODO(nduca): Tooltippish stuff.
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html
new file mode 100644
index 00000000000..fcc73e1d608
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_event_summary_test.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_summary.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('summaryRowNoCpu', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3}));
+ tsg.pushSlice(newSliceEx({title: 'bb', start: 1, end: 2}));
+ tsg.pushSlice(newSliceEx({title: 'bb', start: 4, end: 5}));
+ tsg.createSubSlices();
+
+ const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+ assert.strictEqual(row.duration, 5);
+ assert.strictEqual(row.selfTime, 4);
+ assert.isUndefined(row.cpuDuration);
+ assert.isUndefined(row.cpuSelfTime);
+ });
+
+ test('summaryRowWithCpu', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+ cpuStart: 0, cpuEnd: 3}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+ cpuStart: 1, cpuEnd: 1.75}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+ cpuStart: 3, cpuEnd: 3.75}));
+ tsg.createSubSlices();
+
+ const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+ assert.strictEqual(row.duration, 5);
+ assert.strictEqual(row.selfTime, 4);
+ assert.strictEqual(row.cpuDuration, 4.5);
+ assert.strictEqual(row.cpuSelfTime, 3.75);
+ assert.strictEqual(row.maxDuration, 3);
+ assert.strictEqual(row.maxSelfTime, 2);
+ assert.strictEqual(row.maxCpuDuration, 3);
+ assert.strictEqual(row.maxCpuSelfTime, 2.25);
+ });
+
+ test('summaryRowNonSlice', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+ const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {});
+ model.flowEvents.push(fe1);
+ model.flowEvents.push(fe2);
+
+ const row = new tr.ui.analysis.MultiEventSummary('a', [fe1, fe2]);
+ assert.strictEqual(row.duration, 0);
+ assert.strictEqual(row.selfTime, 0);
+ assert.isUndefined(row.cpuDuration);
+ assert.isUndefined(row.cpuSelfTime);
+ assert.strictEqual(row.maxDuration, 0);
+ });
+
+ test('summaryNumAlerts', function() {
+ const slice = newSliceEx({title: 'b', start: 0, duration: 0.002});
+
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'Critical alert');
+
+ const alert = new tr.model.Alert(ALERT_INFO_1, 5, [slice]);
+
+ const row = new tr.ui.analysis.MultiEventSummary('a', [slice]);
+ assert.strictEqual(row.numAlerts, 1);
+ });
+
+ test('argSummary', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+ args: {value1: 3, value2: 'x', value3: 1}}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+ args: {value1: 3, value2: 'y', value3: 2}}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+ args: {value1: 3, value2: 'z', value3: 'x'}}));
+ tsg.createSubSlices();
+
+ const row = new tr.ui.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+ assert.deepEqual(row.totalledArgs, {value1: 9});
+ assert.deepEqual(row.untotallableArgs, ['value2', 'value3']);
+ assert.strictEqual(row.maxDuration, 3);
+ assert.strictEqual(row.maxSelfTime, 2);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html
new file mode 100644
index 00000000000..3e509508732
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<dom-module id='tr-ui-a-multi-flow-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ </style>
+ <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-flow-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.$.content.eventsHaveDuration = false;
+ this.$.content.eventsHaveSubRows = false;
+ },
+
+ set selection(selection) {
+ this.$.content.selection = selection;
+ },
+
+ get selection() {
+ return this.$.content.selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-flow-event-sub-view',
+ tr.model.FlowEvent,
+ {
+ multi: true,
+ title: 'Flow Events',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html
new file mode 100644
index 00000000000..8b1d98d7bd1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_flow_event_sub_view_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+
+ test('analyzeSelectionWithSingleEvent', function() {
+ const model = new Model();
+
+ const fe1 = new tr.model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+ const fe2 = new tr.model.FlowEvent('cat', 1234, 'title', 8, 20, {});
+ model.flowEvents.push(fe1);
+ model.flowEvents.push(fe2);
+
+ const selection = new EventSet();
+ selection.push(fe1);
+ selection.push(fe2);
+ assert.strictEqual(selection.length, 2);
+
+ const subView = document.createElement('tr-ui-a-multi-flow-event-sub-view');
+ subView.selection = selection;
+
+ this.addHTMLOutput(subView);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html
new file mode 100644
index 00000000000..cdf71245df3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_frame_sub_view.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-frame-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ set selection(selection) {
+ Polymer.dom(this).textContent = '';
+ const realView = document.createElement('tr-ui-a-multi-event-sub-view');
+ realView.eventsHaveDuration = false;
+ realView.eventsHaveSubRows = false;
+
+ Polymer.dom(this).appendChild(realView);
+ realView.setSelectionWithoutErrorChecks(selection);
+
+ this.currentSelection_ = selection;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+
+ const selection = new tr.model.EventSet();
+ this.currentSelection_.forEach(function(frameEvent) {
+ frameEvent.associatedEvents.forEach(function(event) {
+ selection.push(event);
+ });
+ });
+ return selection;
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-frame-sub-view',
+ tr.model.Frame,
+ {
+ multi: true,
+ title: 'Frames',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html
new file mode 100644
index 00000000000..c2252f90e14
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<dom-module id='tr-ui-a-multi-instant-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ </style>
+ <div id='content'></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-instant-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ set selection(selection) {
+ Polymer.dom(this.$.content).textContent = '';
+ const realView = document.createElement('tr-ui-a-multi-event-sub-view');
+ realView.eventsHaveDuration = false;
+ realView.eventsHaveSubRows = false;
+
+ Polymer.dom(this.$.content).appendChild(realView);
+ realView.setSelectionWithoutErrorChecks(selection);
+
+ this.currentSelection_ = selection;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html
new file mode 100644
index 00000000000..ca228515de5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_instant_event_sub_view_test.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+
+ test('analyzeSelectionWithSingleEvent', function() {
+ const model = new Model();
+ const p52 = model.getOrCreateProcess(52);
+ const t53 = p52.getOrCreateThread(53);
+
+ const ie1 = new tr.model.ProcessInstantEvent('cat', 'title', 7, 10, {});
+ const ie2 = new tr.model.ProcessInstantEvent('cat', 'title', 7, 20, {});
+ p52.instantEvents.push(ie1);
+ p52.instantEvents.push(ie2);
+
+
+ const selection = new EventSet();
+ selection.push(ie1);
+ selection.push(ie2);
+ assert.strictEqual(selection.length, 2);
+
+ const subView =
+ document.createElement('tr-ui-a-multi-instant-event-sub-view');
+ subView.selection = selection;
+
+ this.addHTMLOutput(subView);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html
new file mode 100644
index 00000000000..089f4b011a2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-multi-object-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="content"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-object-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ ready() {
+ this.$.content.showHeader = false;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+
+ const objectEvents = Array.from(selection).sort(
+ tr.b.math.Range.compareByMinTimes);
+
+ const timeSpanConfig = {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument
+ };
+ const table = this.$.content;
+ table.tableColumns = [
+ {
+ title: 'First',
+ value(event) {
+ if (event instanceof tr.model.ObjectSnapshot) {
+ return tr.v.ui.createScalarSpan(event.ts, timeSpanConfig);
+ }
+
+ const spanEl = document.createElement('span');
+ Polymer.dom(spanEl).appendChild(tr.v.ui.createScalarSpan(
+ event.creationTs, timeSpanConfig));
+ Polymer.dom(spanEl).appendChild(tr.ui.b.createSpan({
+ textContent: '-',
+ marginLeft: '4px',
+ marginRight: '4px'
+ }));
+ if (event.deletionTs !== Number.MAX_VALUE) {
+ Polymer.dom(spanEl).appendChild(tr.v.ui.createScalarSpan(
+ event.deletionTs, timeSpanConfig));
+ }
+ return spanEl;
+ },
+ width: '200px'
+ },
+ {
+ title: 'Second',
+ value(event) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(function() {
+ return new tr.model.EventSet(event);
+ }, event.userFriendlyName);
+ return linkEl;
+ },
+ width: '100%'
+ }
+ ];
+ table.tableRows = objectEvents;
+ table.rebuild();
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-object-sub-view',
+ tr.model.ObjectInstance,
+ {
+ multi: true,
+ title: 'Object Instances',
+ });
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-object-sub-view',
+ tr.model.ObjectSnapshot,
+ {
+ multi: true,
+ title: 'Object Snapshots',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html
new file mode 100644
index 00000000000..18e62170e7d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_object_sub_view_test.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const ObjectInstance = tr.model.ObjectInstance;
+
+ test('instantiate_analysisWithObjects', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const objects = p1.objects;
+ const i10 = objects.idWasCreated(
+ '0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10);
+ const s10 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl',
+ 10, 'snapshot-1');
+ const s25 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl',
+ 25, 'snapshot-2');
+ const s40 = objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl',
+ 40, 'snapshot-3');
+ objects.idWasDeleted('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 45);
+
+ const track = {};
+ const selection = new EventSet();
+ selection.push(i10);
+ selection.push(s10);
+ selection.push(s25);
+ selection.push(s40);
+
+ const analysisEl = document.createElement('tr-ui-a-multi-object-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html
new file mode 100644
index 00000000000..32315c220a1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/frame_power_usage_chart.html">
+<link rel="import" href="/tracing/ui/analysis/power_sample_summary_table.html">
+
+<dom-module id='tr-ui-a-multi-power-sample-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ }
+ #tables {
+ display: flex;
+ flex-direction: column;
+ width: 50%;
+ }
+ #chart {
+ width: 50%;
+ }
+ </style>
+ <div id="tables">
+ <tr-ui-a-power-sample-summary-table id="summaryTable">
+ </tr-ui-a-power-sample-summary-table>
+ </div>
+ <tr-ui-a-frame-power-usage-chart id="chart">
+ </tr-ui-a-frame-power-usage-chart>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+// TODO(charliea): Add a dropdown that allows the user to select which type of
+// power sample analysis view they want (e.g. table of samples, graph).
+Polymer({
+ is: 'tr-ui-a-multi-power-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ const samples = this.selection;
+ const vSyncTimestamps = (!samples ? [] :
+ tr.b.getFirstElement(samples).series.device.vSyncTimestamps);
+
+ this.$.summaryTable.samples = samples;
+ this.$.chart.setData(this.selection, vSyncTimestamps);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-power-sample-sub-view',
+ tr.model.PowerSample,
+ {
+ multi: true,
+ title: 'Power Samples',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html
new file mode 100644
index 00000000000..f7759572e28
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_power_sample_sub_view_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_noSamplesOrVSyncs', function() {
+ const viewEl = document.createElement(
+ 'tr-ui-a-multi-power-sample-sub-view');
+ viewEl.selection = undefined;
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('instantiate_noVSyncs', function() {
+ const model = new tr.Model();
+ const series = new tr.model.PowerSeries(model.device);
+
+ model.device.vSyncTimestamps = [];
+ series.addPowerSample(1, 1);
+ series.addPowerSample(2, 2);
+ series.addPowerSample(3, 3);
+ series.addPowerSample(4, 2);
+
+ const view = document.createElement('tr-ui-a-multi-power-sample-sub-view');
+ const eventSet = new tr.model.EventSet(series.samples);
+ view.selection = eventSet;
+
+ this.addHTMLOutput(view);
+
+ assert.deepEqual(view.$.chart.samples, eventSet);
+ assert.sameDeepMembers(view.$.chart.vSyncTimestamps, []);
+ });
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const series = new tr.model.PowerSeries(model.device);
+
+ model.device.vSyncTimestamps = [0];
+ series.addPowerSample(1, 1);
+ series.addPowerSample(2, 2);
+ series.addPowerSample(3, 3);
+ series.addPowerSample(4, 2);
+
+ const view = document.createElement('tr-ui-a-multi-power-sample-sub-view');
+ const eventSet = new tr.model.EventSet(series.samples);
+ view.selection = eventSet;
+
+ this.addHTMLOutput(view);
+
+ assert.deepEqual(view.$.chart.samples, eventSet);
+ assert.sameDeepMembers(view.$.chart.vSyncTimestamps, [0]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html
new file mode 100644
index 00000000000..1737894f875
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/multi_dimensional_view.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-multi-sample-sub-view'>
+ <template>
+ <style>
+ :host { display: block; }
+ #control {
+ background-color: #e6e6e6;
+ background-image: -webkit-gradient(linear, 0 0, 0 100%,
+ from(#E5E5E5), to(#D1D1D1));
+ flex: 0 0 auto;
+ overflow-x: auto;
+ }
+ #control::-webkit-scrollbar { height: 0px; }
+ #control {
+ font-size: 12px;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ margin: 1px;
+ margin-right: 2px;
+ }
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <div id="control">
+ Sample View Option
+ </div>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+(function() {
+ const MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder;
+
+ Polymer({
+ is: 'tr-ui-a-multi-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.viewOption_ = undefined;
+ this.selection_ = undefined;
+ },
+
+ ready() {
+ const viewSelector = tr.ui.b.createSelector(
+ this, 'viewOption', 'tracing.ui.analysis.multi_sample_sub_view',
+ MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW,
+ [
+ {
+ label: 'Top-down (Tree)',
+ value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW
+ },
+ {
+ label: 'Top-down (Heavy)',
+ value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW
+ },
+ {
+ label: 'Bottom-up (Heavy)',
+ value: MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW
+ }
+ ]);
+ Polymer.dom(this.$.control).appendChild(viewSelector);
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+ this.updateContents_();
+ },
+
+ get viewOption() {
+ return this.viewOption_;
+ },
+
+ set viewOption(viewOption) {
+ this.viewOption_ = viewOption;
+ this.updateContents_();
+ },
+
+ createSamplingSummary_(selection, viewOption) {
+ const builder = new MultiDimensionalViewBuilder(
+ 1 /* dimensions */, 1 /* valueCount */);
+ const samples = selection.filter(
+ event => event instanceof tr.model.Sample);
+
+ samples.forEach(function(sample) {
+ builder.addPath([sample.userFriendlyStack.reverse()],
+ [1], MultiDimensionalViewBuilder.ValueKind.SELF);
+ });
+
+ return builder.buildView(viewOption);
+ },
+
+ processSampleRows_(rows) {
+ for (const row of rows) {
+ let title = row.title[0];
+ let results = /(.*) (Deoptimized reason: .*)/.exec(title);
+ if (results !== null) {
+ row.deoptReason = results[2];
+ title = results[1];
+ }
+ results = /(.*) url: (.*)/.exec(title);
+ if (results !== null) {
+ row.functionName = results[1];
+ row.url = results[2];
+ if (row.functionName === '') {
+ row.functionName = '(anonymous function)';
+ }
+ if (row.url === '') {
+ row.url = 'unknown';
+ }
+ } else {
+ row.functionName = title;
+ row.url = 'unknown';
+ }
+ this.processSampleRows_(row.subRows);
+ }
+ },
+
+ updateContents_() {
+ if (this.selection === undefined) {
+ this.$.table.tableColumns = [];
+ this.$.table.tableRows = [];
+ this.$.table.rebuild();
+ return;
+ }
+
+ const samplingData = this.createSamplingSummary_(
+ this.selection, this.viewOption);
+ const total = samplingData.values[0].total;
+ const columns = [
+ this.createPercentColumn_('Total', total),
+ this.createSamplesColumn_('Total'),
+ this.createPercentColumn_('Self', total),
+ this.createSamplesColumn_('Self'),
+ {
+ title: 'Function Name',
+ value(row) {
+ // For function that got deoptimized, show function name
+ // as red italic with a tooltip
+ if (row.deoptReason !== undefined) {
+ const spanEl = tr.ui.b.createSpan({
+ italic: true,
+ color: '#F44336',
+ tooltip: row.deoptReason
+ });
+ spanEl.innerText = row.functionName;
+ return spanEl;
+ }
+ return row.functionName;
+ },
+ width: '150px',
+ cmp: (a, b) => a.functionName.localeCompare(b.functionName),
+ showExpandButtons: true
+ },
+ {
+ title: 'Location',
+ value(row) { return row.url; },
+ width: '250px',
+ cmp: (a, b) => a.url.localeCompare(b.url),
+ }
+ ];
+
+ this.processSampleRows_(samplingData.subRows);
+ this.$.table.tableColumns = columns;
+ this.$.table.sortColumnIndex = 1; /* Total samples */
+ this.$.table.sortDescending = true;
+ this.$.table.tableRows = samplingData.subRows;
+ this.$.table.rebuild();
+ },
+
+ createPercentColumn_(title, samplingDataTotal) {
+ const field = title.toLowerCase();
+ return {
+ title: title + ' percent',
+ value(row) {
+ return tr.v.ui.createScalarSpan(
+ row.values[0][field] / samplingDataTotal, {
+ customContextRange: tr.b.math.Range.PERCENT_RANGE,
+ unit: tr.b.Unit.byName.normalizedPercentage,
+ context: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
+ });
+ },
+ width: '60px',
+ cmp: (a, b) => a.values[0][field] - b.values[0][field]
+ };
+ },
+
+ createSamplesColumn_(title) {
+ const field = title.toLowerCase();
+ return {
+ title: title + ' samples',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.values[0][field], {
+ unit: tr.b.Unit.byName.unitlessNumber,
+ context: { maximumFractionDigits: 0 },
+ });
+ },
+ width: '60px',
+ cmp: (a, b) => a.values[0][field] - b.values[0][field]
+ };
+ }
+ });
+
+ tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-sample-sub-view',
+ tr.model.Sample,
+ {
+ multi: true,
+ title: 'Samples',
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html
new file mode 100644
index 00000000000..e148a2d7c95
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_sample_sub_view_test.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/multi_sample_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSampleNamed = tr.c.TestUtils.newSampleNamed;
+
+ function instantiateWithTraces(traces) {
+ let t53;
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ traces.forEach(function(trace, index) {
+ model.samples.push(
+ newSampleNamed(t53, 'X', 'cat', trace, index * 0.02));
+ });
+ }
+ });
+
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new tr.model.EventSet();
+ for (let i = 0; i < t53.samples.length; i++) {
+ selection.push(t53.samples[i]);
+ }
+
+ const view = document.createElement('tr-ui-a-multi-sample-sub-view');
+ view.style.height = '500px';
+ this.addHTMLOutput(view);
+ view.selection = selection;
+ return view;
+ }
+
+ test('instantiate_flat', function() {
+ instantiateWithTraces.call(this, [
+ ['BBB'],
+ ['AAA'],
+ ['AAA'],
+ ['Sleeping'],
+ ['BBB'],
+ ['AAA'],
+ ['CCC'],
+ ['Sleeping']
+ ]);
+ });
+
+ test('instantiate_nested', function() {
+ instantiateWithTraces.call(this, [
+ ['AAA', 'BBB'],
+ ['AAA', 'BBB', 'CCC'],
+ ['AAA', 'BBB'],
+ ['BBB', 'AAA', 'BBB'],
+ ['BBB', 'AAA', 'BBB'],
+ ['BBB', 'AAA', 'BBB']
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html
new file mode 100644
index 00000000000..b896a7bf699
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+
+<dom-module id='tr-ui-a-multi-thread-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #content {
+ display: flex;
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+ #content > tr-ui-a-related-events {
+ margin-left: 8px;
+ flex: 0 1 200px;
+ }
+ </style>
+ <div id="content"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.selection_ = undefined;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+
+ // TODO(nduca): This is a gross hack for cc Frame Viewer, but its only
+ // the frame viewer that needs this feature, so ~shrug~.
+ // We check for its presence so that we do not have a hard dependency
+ // on frame viewer.
+ if (tr.isExported('tr.ui.e.chrome.cc.RasterTaskSelection')) {
+ if (tr.ui.e.chrome.cc.RasterTaskSelection.supports(selection)) {
+ const ltvSelection = new tr.ui.e.chrome.cc.RasterTaskSelection(
+ selection);
+
+ const ltv = new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView();
+ ltv.objectSnapshot = ltvSelection.containingSnapshot;
+ ltv.selection = ltvSelection;
+ ltv.extraHighlightsByLayerId = ltvSelection.extraHighlightsByLayerId;
+
+ Polymer.dom(this.$.content).textContent = '';
+ Polymer.dom(this.$.content).appendChild(ltv);
+
+ this.requiresTallView_ = true;
+ return;
+ }
+ }
+
+ Polymer.dom(this.$.content).textContent = '';
+
+ const mesv = document.createElement('tr-ui-a-multi-event-sub-view');
+ mesv.selection = selection;
+ Polymer.dom(this.$.content).appendChild(mesv);
+
+ const relatedEvents = document.createElement('tr-ui-a-related-events');
+ relatedEvents.setRelatedEvents(selection);
+
+ if (relatedEvents.hasRelatedEvents()) {
+ Polymer.dom(this.$.content).appendChild(relatedEvents);
+ }
+ },
+
+ get requiresTallView() {
+ if (this.$.content.children.length === 0) return false;
+ const childTagName = this.$.content.children[0].tagName;
+ if (childTagName === 'TR-UI-A-MULTI-EVENT-SUB-VIEW') {
+ return false;
+ }
+
+ // Using raster task view.
+ return true;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-thread-slice-sub-view',
+ tr.model.ThreadSlice,
+ {
+ multi: true,
+ title: 'Slices',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html
new file mode 100644
index 00000000000..a44419cf536
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_slice_sub_view_test.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/analysis/multi_thread_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ t53.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0.0, duration: 0.5,
+ type: tr.model.ThreadSlice}));
+ t53.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 1.0, duration: 2,
+ type: tr.model.ThreadSlice}));
+ t53.sliceGroup.createSubSlices();
+
+ const selection = new tr.model.EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+ selection.push(t53.sliceGroup.slices[1]);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-multi-thread-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('withFlows', function() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+
+ m.t2 = m.p1.getOrCreateThread(2);
+ m.t3 = m.p1.getOrCreateThread(3);
+ m.t4 = m.p1.getOrCreateThread(4);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5,
+ type: tr.model.ThreadSlice}));
+ m.sB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15,
+ type: tr.model.ThreadSlice}));
+ m.sC = m.t4.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20,
+ type: tr.model.ThreadSlice}));
+
+ m.t2.createSubSlices();
+ m.t3.createSubSlices();
+ m.t4.createSubSlices();
+
+ m.f1 = newFlowEventEx({
+ title: 'flowish', start: 0, end: 10,
+ startSlice: m.sA,
+ endSlice: m.sB
+ });
+ m.f2 = newFlowEventEx({
+ title: 'flowish', start: 15, end: 21,
+ startSlice: m.sB,
+ endSlice: m.sC
+ });
+ });
+
+ const selection = new tr.model.EventSet();
+ selection.push(m.sA);
+ selection.push(m.sB);
+ selection.push(m.sC);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-multi-thread-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html
new file mode 100644
index 00000000000..f1f0666fc43
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+
+<dom-module id='tr-ui-a-multi-thread-time-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #content {
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+ </style>
+ <tr-ui-a-multi-event-sub-view id="content"></tr-ui-a-multi-event-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-thread-time-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.$.content.eventsHaveSubRows = false;
+ },
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.setSelectionWithoutErrorChecks(selection);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-thread-time-slice-sub-view',
+ tr.model.ThreadTimeSlice,
+ {
+ multi: true,
+ title: 'Thread Timeslices',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html
new file mode 100644
index 00000000000..e3489fe1c9c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_thread_time_slice_sub_view_test.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/multi_thread_time_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBasicModel() {
+ const lines = [
+ 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('instantiate', function() {
+ const m = createBasicModel();
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+
+ const selection = new tr.model.EventSet();
+ selection.push(thread.timeSlices[0]);
+ selection.push(thread.timeSlices[1]);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-multi-thread-time-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html
new file mode 100644
index 00000000000..b89e3fd0724
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/multi_user_expectation_sub_view.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_event_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/user_expectation_related_samples_table.html">
+
+<dom-module id='tr-ui-a-multi-user-expectation-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex: 1 1 auto;
+ }
+ #events {
+ margin-left: 8px;
+ flex: 0 1 200px;
+ }
+ </style>
+ <tr-ui-a-multi-event-sub-view id="realView"></tr-ui-a-multi-event-sub-view>
+ <div id="events">
+ <tr-ui-a-user-expectation-related-samples-table id="relatedSamples"></tr-ui-a-user-expectation-related-samples-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-multi-interaction-record-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.$.realView.setSelectionWithoutErrorChecks(selection);
+
+ this.currentSelection_ = selection;
+
+ this.$.relatedSamples.selection = selection;
+ if (this.$.relatedSamples.hasRelatedSamples()) {
+ this.$.events.style.display = '';
+ } else {
+ this.$.events.style.display = 'none';
+ }
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+
+ const selection = new tr.model.EventSet();
+ this.currentSelection_.forEach(function(ir) {
+ ir.associatedEvents.forEach(function(event) {
+ selection.push(event);
+ });
+ });
+ return selection;
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-user-expectation-sub-view',
+ tr.model.um.UserExpectation,
+ {
+ multi: true,
+ title: 'User Expectations',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html
new file mode 100644
index 00000000000..3c76dc8c9e3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_instance_view.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const ObjectInstanceView = tr.ui.b.define('object-instance-view');
+
+ ObjectInstanceView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.objectInstance_ = undefined;
+ },
+
+ get requiresTallView() {
+ return true;
+ },
+
+ set modelEvent(obj) {
+ this.objectInstance = obj;
+ },
+
+ get modelEvent() {
+ return this.objectInstance;
+ },
+
+ get objectInstance() {
+ return this.objectInstance_;
+ },
+
+ set objectInstance(i) {
+ this.objectInstance_ = i;
+ this.updateContents();
+ },
+
+ updateContents() {
+ throw new Error('Not implemented');
+ }
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = ObjectInstanceView;
+ options.defaultMetadata = {
+ showInTrackView: true
+ };
+ tr.b.decorateExtensionRegistry(ObjectInstanceView, options);
+
+ return {
+ ObjectInstanceView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html
new file mode 100644
index 00000000000..a42ed0ec02b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/object_snapshot_view.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const ObjectSnapshotView = tr.ui.b.define('object-snapshot-view');
+
+ ObjectSnapshotView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.objectSnapshot_ = undefined;
+ },
+
+ get requiresTallView() {
+ return true;
+ },
+
+ set modelEvent(obj) {
+ this.objectSnapshot = obj;
+ },
+
+ get modelEvent() {
+ return this.objectSnapshot;
+ },
+
+ get objectSnapshot() {
+ return this.objectSnapshot_;
+ },
+
+ set objectSnapshot(i) {
+ this.objectSnapshot_ = i;
+ this.updateContents();
+ },
+
+ updateContents() {
+ throw new Error('Not implemented');
+ }
+ };
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ options.mandatoryBaseClass = ObjectSnapshotView;
+ options.defaultMetadata = {
+ showInstances: true,
+ showInTrackView: true
+ };
+ tr.b.decorateExtensionRegistry(ObjectSnapshotView, options);
+
+ return {
+ ObjectSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html
new file mode 100644
index 00000000000..337e4f5ba56
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-power-sample-summary-table'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-power-sample-summary-table',
+
+ ready() {
+ this.$.table.tableColumns = [
+ {
+ title: 'Min power',
+ width: '100px',
+ value(row) {
+ return tr.b.Unit.byName.powerInWatts.format(row.min);
+ }
+ },
+ {
+ title: 'Max power',
+ width: '100px',
+ value(row) {
+ return tr.b.Unit.byName.powerInWatts.format(row.max);
+ }
+ },
+ {
+ title: 'Time-weighted average',
+ width: '100px',
+ value(row) {
+ return tr.b.Unit.byName.powerInWatts.format(
+ row.timeWeightedAverageInW);
+ }
+ },
+ {
+ title: 'Energy consumed',
+ width: '100px',
+ value(row) {
+ return tr.b.Unit.byName.energyInJoules.format(row.energyConsumedInJ);
+ }
+ },
+ {
+ title: 'Sample count',
+ width: '100%',
+ value(row) { return row.sampleCount; }
+ }
+ ];
+ this.samples = new tr.model.EventSet();
+ },
+
+ get samples() {
+ return this.samples_;
+ },
+
+ set samples(samples) {
+ if (samples === this.samples) return;
+
+ this.samples_ =
+ (samples === undefined) ? new tr.model.EventSet() : samples;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ if (this.samples.length === 0) {
+ this.$.table.tableRows = [];
+ } else {
+ this.$.table.tableRows = [{
+ min: this.getMin(),
+ max: this.getMax(),
+ timeWeightedAverageInW: this.getTimeWeightedAverageInW(),
+ energyConsumedInJ: this.getEnergyConsumedInJ(),
+ sampleCount: this.samples.length
+ }];
+ }
+
+ this.$.table.rebuild();
+ },
+
+ getMin() {
+ return Math.min.apply(null, this.samples.map(function(sample) {
+ return sample.powerInW;
+ }));
+ },
+
+ getMax() {
+ return Math.max.apply(null, this.samples.map(function(sample) {
+ return sample.powerInW;
+ }));
+ },
+
+ /**
+ * Returns a time-weighted average of the power consumption (Watts)
+ * in between the first sample (inclusive) and last sample (exclusive).
+ */
+ getTimeWeightedAverageInW() {
+ const energyConsumedInJ = this.getEnergyConsumedInJ();
+
+ if (energyConsumedInJ === 'N/A') return 'N/A';
+
+ const durationInS = tr.b.convertUnit(this.samples.bounds.duration,
+ tr.b.UnitPrefixScale.METRIC.MILLI,
+ tr.b.UnitPrefixScale.METRIC.NONE);
+
+ return energyConsumedInJ / durationInS;
+ },
+
+
+ getEnergyConsumedInJ() {
+ if (this.samples.length < 2) return 'N/A';
+
+ const bounds = this.samples.bounds;
+ const series = tr.b.getFirstElement(this.samples).series;
+ return series.getEnergyConsumedInJ(bounds.min, bounds.max);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html
new file mode 100644
index 00000000000..f5bf8c7d5a0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/power_sample_summary_table_test.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/power_sample_summary_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+
+ test('instantiate', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+ series.addPowerSample(2000, 3);
+ series.addPowerSample(3000, 4);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ this.addHTMLOutput(table);
+ });
+
+ test('setSamples_undefinedPowerSamples', function() {
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = undefined;
+
+ assert.lengthOf(table.$.table.tableRows, 0);
+ });
+
+ test('setSamples_noPowerSamples', function() {
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet([]);
+
+ assert.lengthOf(table.$.table.tableRows, 0);
+ });
+
+ test('setSamples_onePowerSample', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ assert.lengthOf(table.$.table.tableRows, 1);
+ assert.strictEqual(table.$.table.tableRows[0].min, 1);
+ assert.strictEqual(table.$.table.tableRows[0].max, 1);
+ assert.strictEqual(
+ table.$.table.tableRows[0].timeWeightedAverageInW, 'N/A');
+ assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 'N/A');
+ assert.strictEqual(table.$.table.tableRows[0].sampleCount, 1);
+ });
+
+ test('setSamples_twoPowerSamples', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ assert.lengthOf(table.$.table.tableRows, 1);
+ assert.strictEqual(table.$.table.tableRows[0].min, 1);
+ assert.strictEqual(table.$.table.tableRows[0].max, 2);
+ assert.strictEqual(table.$.table.tableRows[0].timeWeightedAverageInW, 1);
+ assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 1);
+ assert.strictEqual(table.$.table.tableRows[0].sampleCount, 2);
+ });
+
+ test('setSamples_threePowerSamples', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+ series.addPowerSample(2000, 3);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ assert.lengthOf(table.$.table.tableRows, 1);
+ assert.strictEqual(table.$.table.tableRows[0].min, 1);
+ assert.strictEqual(table.$.table.tableRows[0].max, 3);
+ assert.strictEqual(table.$.table.tableRows[0].timeWeightedAverageInW, 1.5);
+ assert.strictEqual(table.$.table.tableRows[0].energyConsumedInJ, 3);
+ assert.strictEqual(table.$.table.tableRows[0].sampleCount, 3);
+ });
+
+ test('setSamples_columnsInitialized', function() {
+ const series = new PowerSeries(new Model().device);
+
+ series.addPowerSample(0, 1);
+ series.addPowerSample(1000, 2);
+ series.addPowerSample(2000, 3);
+
+ const table = document.createElement('tr-ui-a-power-sample-summary-table');
+ table.samples = new EventSet(series.samples);
+
+ const row = table.$.table.tableRows[0];
+ const columns = table.$.table.tableColumns;
+
+ assert.lengthOf(columns, 5);
+
+ assert.strictEqual(columns[0].title, 'Min power');
+ assert.strictEqual(columns[0].width, '100px');
+ assert.strictEqual(columns[0].value(row), '1.000 W');
+
+ assert.strictEqual(columns[1].title, 'Max power');
+ assert.strictEqual(columns[1].width, '100px');
+ assert.strictEqual(columns[1].value(row), '3.000 W');
+
+ assert.strictEqual(columns[2].title, 'Time-weighted average');
+ assert.strictEqual(columns[2].width, '100px');
+ assert.strictEqual(columns[2].value(row), '1.500 W');
+
+ assert.strictEqual(columns[3].title, 'Energy consumed');
+ assert.strictEqual(columns[3].width, '100px');
+ assert.strictEqual(columns[3].value(row), '3.000 J');
+
+ assert.strictEqual(columns[4].title, 'Sample count');
+ assert.strictEqual(columns[4].width, '100%');
+ assert.strictEqual(columns[4].value(row), 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html
new file mode 100644
index 00000000000..62abbe8076b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const RebuildableBehavior = {
+ rebuild() {
+ /**
+ * Rebuild the pane if necessary.
+ *
+ * This method is not intended to be overriden by subclasses. Please
+ * override scheduleRebuild_() instead.
+ */
+ if (!this.paneDirty_) {
+ // Avoid rebuilding unnecessarily as it breaks things like table
+ // selection.
+ return;
+ }
+
+ this.paneDirty_ = false;
+ this.onRebuild_();
+ },
+
+ /**
+ * Mark the UI state of the pane as dirty and schedule a rebuild.
+ *
+ * This method is intended to be called by subclasses.
+ */
+ scheduleRebuild_() {
+ if (this.paneDirty_) return;
+ this.paneDirty_ = true;
+ tr.b.requestAnimationFrame(this.rebuild.bind(this));
+ },
+
+ /**
+ * Called when the pane is dirty and a rebuild is triggered.
+ *
+ * This method is intended to be overriden by subclasses (instead of
+ * directly overriding rebuild()).
+ */
+ onRebuild_() {
+ }
+ };
+
+ return {
+ RebuildableBehavior,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html
new file mode 100644
index 00000000000..2a2f083ceb3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/rebuildable_behavior_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ Polymer({
+ is: 'tr-ui-analysis-rebuildable-test-element',
+ behaviors: [tr.ui.analysis.RebuildableBehavior]
+ });
+
+ test('rebuild', function() {
+ const el = document.createElement(
+ 'tr-ui-analysis-rebuildable-test-element');
+ let didFireOnRebuild;
+ el.onRebuild_ = function() {
+ assert.strictEqual(this, el);
+ didFireOnRebuild = true;
+ };
+
+ function checkManualRebuild(expectedDidFireOnRebuild) {
+ didFireOnRebuild = false;
+ el.rebuild();
+ assert.strictEqual(didFireOnRebuild, expectedDidFireOnRebuild);
+ }
+
+ function checkRAFRebuild(expectedDidFireOnRebuild) {
+ didFireOnRebuild = false;
+ tr.b.forcePendingRAFTasksToRun();
+ assert.strictEqual(didFireOnRebuild, expectedDidFireOnRebuild);
+ }
+
+ // No rebuilds should occur when not scheduled.
+ checkManualRebuild(false);
+ checkRAFRebuild(false);
+
+ // Single rebuild should occur when scheduled once.
+ el.scheduleRebuild_();
+ checkManualRebuild(true);
+ checkManualRebuild(false);
+
+ el.scheduleRebuild_();
+ checkRAFRebuild(true);
+ checkRAFRebuild(false);
+
+ // Only a single rebuild should occur even when scheduled multiple times.
+ el.scheduleRebuild_();
+ el.scheduleRebuild_();
+ checkManualRebuild(true);
+ checkRAFRebuild(false);
+ checkManualRebuild(false);
+
+ el.scheduleRebuild_();
+ el.scheduleRebuild_();
+ checkRAFRebuild(true);
+ checkRAFRebuild(false);
+ checkManualRebuild(false);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html
new file mode 100644
index 00000000000..b4036837bf5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events.html
@@ -0,0 +1,354 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/flow_classifier.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-related-events'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+function* getEventInFlowEvents(event) {
+ if (!event.inFlowEvents) return;
+ yield* event.inFlowEvents;
+}
+
+function* getEventOutFlowEvents(event) {
+ if (!event.outFlowEvents) return;
+ yield* event.outFlowEvents;
+}
+
+function* getEventAncestors(event) {
+ if (!event.enumerateAllAncestors) return;
+ yield* event.enumerateAllAncestors();
+}
+
+function* getEventDescendents(event) {
+ if (!event.enumerateAllDescendents) return;
+ yield* event.enumerateAllDescendents();
+}
+
+Polymer({
+ is: 'tr-ui-a-related-events',
+
+ ready() {
+ this.eventGroups_ = [];
+ this.cancelFunctions_ = [];
+
+ this.$.table.tableColumns = [
+ {
+ title: 'Event(s)',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.type;
+ if (row.tooltip) {
+ typeEl.title = row.tooltip;
+ }
+ return typeEl;
+ },
+ width: '150px'
+ },
+ {
+ title: 'Link',
+ width: '100%',
+ value(row) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ if (row.name) {
+ linkEl.setSelectionAndContent(row.selection, row.name);
+ } else {
+ linkEl.selection = row.selection;
+ }
+ return linkEl;
+ }
+ }
+ ];
+ },
+
+ hasRelatedEvents() {
+ return (this.eventGroups_ && this.eventGroups_.length > 0);
+ },
+
+ setRelatedEvents(eventSet) {
+ this.cancelAllTasks_();
+ this.eventGroups_ = [];
+ this.addRuntimeCallStats_(eventSet);
+ this.addOverlappingV8ICStats_(eventSet);
+ this.addV8GCObjectStats_(eventSet);
+ this.addV8Slices_(eventSet);
+ this.addConnectedFlows_(eventSet);
+ this.addConnectedEvents_(eventSet);
+ this.addOverlappingSamples_(eventSet);
+ this.updateContents_();
+ },
+
+ addConnectedFlows_(eventSet) {
+ const classifier = new tr.ui.analysis.FlowClassifier();
+ eventSet.forEach(function(slice) {
+ if (slice.inFlowEvents) {
+ slice.inFlowEvents.forEach(function(flow) {
+ classifier.addInFlow(flow);
+ });
+ }
+ if (slice.outFlowEvents) {
+ slice.outFlowEvents.forEach(function(flow) {
+ classifier.addOutFlow(flow);
+ });
+ }
+ });
+ if (!classifier.hasEvents()) return;
+
+ const addToEventGroups = function(type, flowEvent) {
+ this.eventGroups_.push({
+ type,
+ selection: new tr.model.EventSet(flowEvent),
+ name: flowEvent.title
+ });
+ };
+
+ classifier.inFlowEvents.forEach(
+ addToEventGroups.bind(this, 'Incoming flow'));
+ classifier.outFlowEvents.forEach(
+ addToEventGroups.bind(this, 'Outgoing flow'));
+ classifier.internalFlowEvents.forEach(
+ addToEventGroups.bind(this, 'Internal flow'));
+ },
+
+ cancelAllTasks_() {
+ this.cancelFunctions_.forEach(function(cancelFunction) {
+ cancelFunction();
+ });
+ this.cancelFunctions_ = [];
+ },
+
+ addConnectedEvents_(eventSet) {
+ this.cancelFunctions_.push(this.createEventsLinkIfNeeded_(
+ 'Preceding events',
+ 'Add all events that have led to the selected one(s), connected by ' +
+ 'flow arrows or by call stack.',
+ eventSet,
+ function* (event) {
+ yield* getEventInFlowEvents(event);
+ yield* getEventAncestors(event);
+ if (event.startSlice) {
+ yield event.startSlice;
+ }
+ }.bind(this)));
+ this.cancelFunctions_.push(this.createEventsLinkIfNeeded_(
+ 'Following events',
+ 'Add all events that have been caused by the selected one(s), ' +
+ 'connected by flow arrows or by call stack.',
+ eventSet,
+ function* (event) {
+ yield* getEventOutFlowEvents(event);
+ yield* getEventDescendents(event);
+ if (event.endSlice) {
+ yield event.endSlice;
+ }
+ }.bind(this)));
+ this.cancelFunctions_.push(this.createEventsLinkIfNeeded_(
+ 'All connected events',
+ 'Add all events connected to the selected one(s) by flow arrows or ' +
+ 'by call stack.',
+ eventSet,
+ function* (event) {
+ yield* getEventInFlowEvents(event);
+ yield* getEventOutFlowEvents(event);
+ yield* getEventAncestors(event);
+ yield* getEventDescendents(event);
+ if (event.startSlice) {
+ yield event.startSlice;
+ }
+ if (event.endSlice) {
+ yield event.endSlice;
+ }
+ }.bind(this)));
+ },
+
+ createEventsLinkIfNeeded_(title, tooltip, events, connectedFn) {
+ events = new tr.model.EventSet(events);
+ const eventsToProcess = new Set(events);
+ // for (let event of events)
+ // eventsToProcess.add(event);
+ let wasChanged = false;
+ let task;
+ let isCanceled = false;
+ function addEventsUntilTimeout() {
+ if (isCanceled) return;
+ // Let's grant ourselves a budget of 8 ms. If time runs out, then
+ // create another task to do the rest.
+ const timeout = window.performance.now() + 8;
+ // TODO(alexandermont): Don't check window.performance.now
+ // every iteration.
+ while (eventsToProcess.size > 0 &&
+ window.performance.now() <= timeout) {
+ // Get the next event.
+ const nextEvent = tr.b.getFirstElement(eventsToProcess);
+ eventsToProcess.delete(nextEvent);
+
+ // Add the connected events to the list.
+ for (const eventToAdd of connectedFn(nextEvent)) {
+ if (!events.contains(eventToAdd)) {
+ events.push(eventToAdd);
+ eventsToProcess.add(eventToAdd);
+ wasChanged = true;
+ }
+ }
+ }
+ if (eventsToProcess.size > 0) {
+ // There are still events to process, but we ran out of time. Post
+ // more work for later.
+ const newTask = new tr.b.Task(
+ addEventsUntilTimeout.bind(this), this);
+ task.after(newTask);
+ task = newTask;
+ return;
+ }
+ // Went through all events, add the link.
+ if (!wasChanged) return;
+ this.eventGroups_.push({
+ type: title,
+ tooltip,
+ selection: events
+ });
+ this.updateContents_();
+ }
+ function cancelTask() {
+ isCanceled = true;
+ }
+ task = new tr.b.Task(addEventsUntilTimeout.bind(this), this);
+ tr.b.Task.RunWhenIdle(task);
+ return cancelTask;
+ },
+
+ addOverlappingSamples_(eventSet) {
+ const samples = new tr.model.EventSet();
+ for (const slice of eventSet) {
+ if (!slice.parentContainer || !slice.parentContainer.samples) {
+ continue;
+ }
+ const candidates = slice.parentContainer.samples;
+ const range = tr.b.math.Range.fromExplicitRange(
+ slice.start, slice.start + slice.duration);
+ const filteredSamples = range.filterArray(
+ candidates, function(value) {return value.start;});
+ for (const sample of filteredSamples) {
+ samples.push(sample);
+ }
+ }
+ if (samples.length > 0) {
+ this.eventGroups_.push({
+ type: 'Overlapping samples',
+ tooltip: 'All samples overlapping the selected slice(s).',
+ selection: samples
+ });
+ }
+ },
+
+ addV8Slices_(eventSet) {
+ const v8Slices = new tr.model.EventSet();
+ for (const slice of eventSet) {
+ if (slice.category === 'v8') {
+ v8Slices.push(slice);
+ }
+ }
+ if (v8Slices.length > 0) {
+ this.eventGroups_.push({
+ type: 'V8 Slices',
+ tooltip: 'All V8 slices in the selected slice(s).',
+ selection: v8Slices
+ });
+ }
+ },
+
+ addRuntimeCallStats_(eventSet) {
+ const slices = eventSet.filter(function(slice) {
+ return (slice.category === 'v8' ||
+ slice.category === 'disabled-by-default-v8.runtime_stats') &&
+ slice.runtimeCallStats;
+ });
+ if (slices.length > 0) {
+ this.eventGroups_.push({
+ type: 'Runtime call stats table',
+ // eslint-disable-next-line
+ tooltip: 'All V8 slices containing runtime call stats table in the selected slice(s).',
+ selection: slices
+ });
+ }
+ },
+
+ addV8GCObjectStats_(eventSet) {
+ const slices = new tr.model.EventSet();
+ for (const slice of eventSet) {
+ if (slice.title === 'V8.GC_Objects_Stats') {
+ slices.push(slice);
+ }
+ }
+ if (slices.length > 0) {
+ this.eventGroups_.push({
+ type: 'V8 GC stats table',
+ tooltip: 'All V8 GC statistics slices in the selected set.',
+ selection: slices
+ });
+ }
+ },
+
+ addOverlappingV8ICStats_(eventSet) {
+ const slices = new tr.model.EventSet();
+ for (const slice of eventSet) {
+ if (!slice.parentContainer || !slice.parentContainer.sliceGroup) {
+ continue;
+ }
+ const sliceGroup = slice.parentContainer.sliceGroup.slices;
+ const range = tr.b.math.Range.fromExplicitRange(
+ slice.start, slice.start + slice.duration);
+ const filteredSlices = range.filterArray(
+ sliceGroup, value => value.start);
+ const icSlices = filteredSlices.filter(x => x.title === 'V8.ICStats');
+ for (const icSlice of icSlices) {
+ slices.push(icSlice);
+ }
+ }
+ if (slices.length > 0) {
+ this.eventGroups_.push({
+ type: 'Overlapping V8 IC stats',
+ tooltip: 'All V8 IC statistics overlapping the selected set.',
+ selection: slices
+ });
+ }
+ },
+
+ updateContents_() {
+ const table = this.$.table;
+ if (this.eventGroups_ === undefined) {
+ table.tableRows = [];
+ } else {
+ table.tableRows = this.eventGroups_.slice();
+ }
+ table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html
new file mode 100644
index 00000000000..5d14d68faa3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/related_events_test.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/sample.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+
+ m.t2 = m.p1.getOrCreateThread(2);
+ m.t3 = m.p1.getOrCreateThread(3);
+ m.t4 = m.p1.getOrCreateThread(4);
+ const node = tr.c.TestUtils.newProfileNodes(m, ['fake']);
+
+ // Setup samples and slices in this way:
+ // 0 5 10 15 20
+ // _____________________________
+ // t2 *
+ // [ a ][ ]aa
+ // -----------------------------
+ // t3 * * * * *
+ // * *
+ // [ b ]
+ // [bb]
+ // []bbb
+ // -----------------------------
+ // t4 |c
+ // -----------------------------
+ m.samples.push(
+ new tr.model.Sample(10, 'b10_1', node, m.t3),
+ new tr.model.Sample(7, 'b7', node, m.t3),
+ new tr.model.Sample(12, 'b12', node, m.t3),
+ new tr.model.Sample(20, 'b20', node, m.t3),
+ new tr.model.Sample(10, 'b10_2', node, m.t3),
+ new tr.model.Sample(15, 'b15_1', node, m.t3),
+ new tr.model.Sample(15, 'b15_2', node, m.t3),
+ new tr.model.Sample(12, 'a12', node, m.t2)
+ );
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5,
+ type: tr.model.ThreadSlice}));
+ m.sAA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'aa', start: 6, end: 8,
+ type: tr.model.ThreadSlice}));
+ m.sB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15,
+ type: tr.model.ThreadSlice}));
+ m.sBB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'bb', start: 11, end: 14,
+ type: tr.model.ThreadSlice}));
+ m.sBBB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'bbb', start: 12, end: 13,
+ type: tr.model.ThreadSlice}));
+ m.sC = m.t4.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20,
+ type: tr.model.ThreadSlice}));
+
+ m.t2.createSubSlices();
+ m.t3.createSubSlices();
+ m.t4.createSubSlices();
+
+ // Add flow events.
+ m.f0 = newFlowEventEx({
+ title: 'a_aa', start: 5, end: 6,
+ startSlice: m.sA,
+ endSlice: m.sAA
+ });
+ m.f1 = newFlowEventEx({
+ title: 'a_b', start: 0, end: 10,
+ startSlice: m.sA,
+ endSlice: m.sB
+ });
+ m.f2 = newFlowEventEx({
+ title: 'b_bbb', start: 10, end: 12,
+ startSlice: m.sB,
+ endSlice: m.sBBB
+ });
+ m.f3 = newFlowEventEx({
+ title: 'bbb_c', start: 13, end: 20,
+ startSlice: m.sBBB,
+ endSlice: m.sC
+ });
+ });
+ return m;
+ }
+
+ test('instantiate', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-a-related-events');
+ const selection = new tr.model.EventSet(
+ [m.sA, m.f0, m.sAA, m.f1, m.sB, m.f2, m.sBB, m.sBBB, m.f3, m.sC]);
+ viewEl.setRelatedEvents(selection);
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ // Check that the element handles multiple setRelatedEvents calls correctly.
+ assert.lengthOf(viewEl.$.table.tableRows, 5);
+ viewEl.setRelatedEvents(selection);
+ assert.lengthOf(viewEl.$.table.tableRows, 5);
+ });
+
+ test('validateFlows', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-a-related-events');
+ viewEl.setRelatedEvents(new tr.model.EventSet([m.sB, m.sBB, m.sBBB]));
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let inFlows;
+ let outFlows;
+ let internalFlows;
+ viewEl.$.table.tableRows.forEach(function(row) {
+ if (row.type === 'Incoming flow') {
+ assert.isUndefined(inFlows);
+ inFlows = row.selection;
+ }
+ if (row.type === 'Outgoing flow') {
+ assert.isUndefined(outFlows);
+ outFlows = row.selection;
+ }
+ if (row.type === 'Internal flow') {
+ assert.isUndefined(internalFlows);
+ internalFlows = row.selection;
+ }
+ });
+ assert.strictEqual(inFlows.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(inFlows).title, 'a_b');
+ assert.strictEqual(outFlows.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(outFlows).title, 'bbb_c');
+ assert.strictEqual(internalFlows.length, 1);
+ assert.strictEqual(tr.b.getOnlyElement(internalFlows).title, 'b_bbb');
+ });
+
+ test('validateConnectedEvents', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-a-related-events');
+ viewEl.setRelatedEvents(new tr.model.EventSet([m.sBB]));
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let precedingEvents;
+ let followingEvents;
+ let allEvents;
+ viewEl.$.table.tableRows.forEach(function(row) {
+ if (row.type === 'Preceding events') {
+ assert.isUndefined(precedingEvents);
+ precedingEvents = row.selection;
+ }
+ if (row.type === 'Following events') {
+ assert.isUndefined(followingEvents);
+ followingEvents = row.selection;
+ }
+ if (row.type === 'All connected events') {
+ assert.isUndefined(allEvents);
+ allEvents = row.selection;
+ }
+ });
+
+ const precedingTitles = precedingEvents.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(precedingTitles, ['a', 'a_b', 'b', 'bb']);
+
+ const followingTitles = followingEvents.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(followingTitles, ['bb', 'bbb', 'bbb_c', 'c']);
+
+ const allTitles = allEvents.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(allTitles,
+ ['a', 'a_aa', 'aa', 'a_b', 'b', 'bb', 'bbb', 'b_bbb', 'bbb_c', 'c']);
+ });
+
+ test('validateOverlappingSamples', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-a-related-events');
+ viewEl.setRelatedEvents(new tr.model.EventSet([m.sB]));
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ let overlappingSamples;
+ viewEl.$.table.tableRows.forEach(function(row) {
+ if (row.type === 'Overlapping samples') {
+ assert.isUndefined(overlappingSamples);
+ overlappingSamples = row.selection;
+ }
+ });
+
+ const samplesTitles = overlappingSamples.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(samplesTitles,
+ ['b10_1', 'b10_2', 'b12', 'b15_1', 'b15_2']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html
new file mode 100644
index 00000000000..68ca4d533d4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-selection-summary-table'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-selection-summary-table',
+ created() {
+ this.selection_ = new tr.b.math.Range();
+ },
+
+ ready() {
+ this.$.table.showHeader = false;
+ this.$.table.tableColumns = [
+ {
+ title: 'Name',
+ value(row) { return row.title; },
+ width: '350px'
+ },
+ {
+ title: 'Value',
+ width: '100%',
+ value(row) {
+ return row.value;
+ }
+ }
+ ];
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ const selection = this.selection_;
+ const rows = [];
+ let hasRange;
+ if (this.selection_ && (!selection.bounds.isEmpty)) {
+ hasRange = true;
+ } else {
+ hasRange = false;
+ }
+
+ rows.push({
+ title: 'Selection start',
+ value: hasRange ? tr.v.ui.createScalarSpan(
+ selection.bounds.min, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument
+ }) : '<empty>'
+ });
+ rows.push({
+ title: 'Selection extent',
+ value: hasRange ? tr.v.ui.createScalarSpan(
+ selection.bounds.range, {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ ownerDocument: this.ownerDocument
+ }) : '<empty>'
+ });
+
+ this.$.table.tableRows = rows;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html
new file mode 100644
index 00000000000..20a8daf3b47
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/selection_summary_table_test.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/selection_summary_table.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('noSelection', function() {
+ const summaryTable =
+ document.createElement('tr-ui-a-selection-summary-table');
+ summaryTable.selection = undefined;
+ this.addHTMLOutput(summaryTable);
+
+ const tableEl = tr.ui.b.findDeepElementMatching(
+ summaryTable, 'tr-ui-b-table');
+ assert.strictEqual(tableEl.tableRows[0].value, '<empty>');
+ assert.strictEqual(tableEl.tableRows[1].value, '<empty>');
+ });
+
+ test('emptySelection', function() {
+ const summaryTable =
+ document.createElement('tr-ui-a-selection-summary-table');
+ const selection = new EventSet();
+ summaryTable.selection = selection;
+ this.addHTMLOutput(summaryTable);
+
+ const tableEl = tr.ui.b.findDeepElementMatching(
+ summaryTable, 'tr-ui-b-table');
+ assert.strictEqual(tableEl.tableRows[0].value, '<empty>');
+ assert.strictEqual(tableEl.tableRows[1].value, '<empty>');
+ });
+
+ test('selection', function() {
+ const model = new Model();
+ const thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+ const tsg = thread.sliceGroup;
+
+ tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3}));
+ tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2}));
+
+ const selection = new EventSet();
+ selection.push(tsg.slices[0]);
+ selection.push(tsg.slices[1]);
+
+ const summaryTable =
+ document.createElement('tr-ui-a-selection-summary-table');
+ summaryTable.selection = selection;
+ this.addHTMLOutput(summaryTable);
+
+ const tableEl = tr.ui.b.findDeepElementMatching(
+ summaryTable, 'tr-ui-b-table');
+ assert.strictEqual(tableEl.tableRows[0].value.value, 0);
+ assert.strictEqual(tableEl.tableRows[0].value.unit,
+ tr.b.Unit.byName.timeStampInMs);
+ assert.strictEqual(tableEl.tableRows[1].value.value, 3);
+ assert.strictEqual(tableEl.tableRows[1].value.unit,
+ tr.b.Unit.byName.timeDurationInMs);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html
new file mode 100644
index 00000000000..cc7f7b840dd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id='tr-ui-a-single-async-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ }
+ #events {
+ display:flex;
+ flex-direction: column;
+ }
+ </style>
+ <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view>
+ <div id="events">
+ <tr-ui-a-related-events id="relatedEvents"></tr-ui-a-related-events>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-async-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ if (selection.length !== 1) {
+ throw new Error('Only supports single slices');
+ }
+ this.$.content.setSelectionWithoutErrorChecks(selection);
+ this.$.relatedEvents.setRelatedEvents(selection);
+ if (this.$.relatedEvents.hasRelatedEvents()) {
+ this.$.relatedEvents.style.display = '';
+ } else {
+ this.$.relatedEvents.style.display = 'none';
+ }
+ },
+
+ getEventRows_(event) {
+ // TODO(nduca): Figure out if there is a cleaner way to do this.
+ const rows = this.__proto__.__proto__.getEventRows_(event);
+
+ // Put the ID up top.
+ rows.splice(0, 0, {
+ name: 'ID',
+ value: event.id
+ });
+ return rows;
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+ return tr.b.getOnlyElement(this.currentSelection_).associatedEvents;
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-async-slice-sub-view',
+ tr.model.AsyncSlice,
+ {
+ multi: false,
+ title: 'Async Slice',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html
new file mode 100644
index 00000000000..6cd341011bb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_async_slice_sub_view_test.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_async_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx;
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.asyncSliceGroup.push(newAsyncSliceEx({
+ id: 31415,
+ title: 'a',
+ start: 10,
+ duration: 20,
+ startThread: t1,
+ endThread: t1
+ }));
+
+ const selection = new tr.model.EventSet();
+ selection.push(t1.asyncSliceGroup.slices[0]);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-single-async-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html
new file mode 100644
index 00000000000..12040ca9bad
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-cpu-slice-sub-view'>
+ <template>
+ <style>
+ table {
+ border-collapse: collapse;
+ border-width: 0;
+ margin-bottom: 25px;
+ width: 100%;
+ }
+
+ table tr > td:first-child {
+ padding-left: 2px;
+ }
+
+ table tr > td {
+ padding: 2px 4px 2px 4px;
+ vertical-align: text-top;
+ width: 150px;
+ }
+
+ table td td {
+ padding: 0 0 0 0;
+ width: auto;
+ }
+ tr {
+ vertical-align: top;
+ }
+
+ tr:nth-child(2n+0) {
+ background-color: #e2e2e2;
+ }
+ </style>
+ <table>
+ <tr>
+ <td>Running process:</td><td id="process-name"></td>
+ </tr>
+ <tr>
+ <td>Running thread:</td><td id="thread-name"></td>
+ </tr>
+ <tr>
+ <td>Start:</td>
+ <td>
+ <tr-v-ui-scalar-span id="start">
+ </tr-v-ui-scalar-span>
+ </td>
+ </tr>
+ <tr>
+ <td>Duration:</td>
+ <td>
+ <tr-v-ui-scalar-span id="duration">
+ </tr-v-ui-scalar-span>
+ </td>
+ </tr>
+ <tr>
+ <td>Active slices:</td><td id="running-thread"></td>
+ </tr>
+ <tr>
+ <td>Args:</td>
+ <td>
+ <tr-ui-a-generic-object-view id="args">
+ </tr-ui-a-generic-object-view>
+ </td>
+ </tr>
+ </table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-cpu-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ const cpuSlice = tr.b.getOnlyElement(selection);
+ if (!(cpuSlice instanceof tr.model.CpuSlice)) {
+ throw new Error('Only supports thread time slices');
+ }
+
+ this.currentSelection_ = selection;
+
+ const thread = cpuSlice.threadThatWasRunning;
+
+ const root = Polymer.dom(this.root);
+ if (thread) {
+ Polymer.dom(root.querySelector('#process-name')).textContent =
+ thread.parent.userFriendlyName;
+ Polymer.dom(root.querySelector('#thread-name')).textContent =
+ thread.userFriendlyName;
+ } else {
+ root.querySelector('#process-name').parentElement.style.display =
+ 'none';
+ Polymer.dom(root.querySelector('#thread-name')).textContent =
+ cpuSlice.title;
+ }
+
+ root.querySelector('#start').setValueAndUnit(
+ cpuSlice.start, tr.b.Unit.byName.timeStampInMs);
+ root.querySelector('#duration').setValueAndUnit(
+ cpuSlice.duration, tr.b.Unit.byName.timeDurationInMs);
+
+ const runningThreadEl = root.querySelector('#running-thread');
+
+ const timeSlice = cpuSlice.getAssociatedTimeslice();
+ if (!timeSlice) {
+ runningThreadEl.parentElement.style.display = 'none';
+ } else {
+ const threadLink = document.createElement('tr-ui-a-analysis-link');
+ threadLink.selection = new tr.model.EventSet(timeSlice);
+ Polymer.dom(threadLink).textContent = 'Click to select';
+ runningThreadEl.parentElement.style.display = '';
+ Polymer.dom(runningThreadEl).textContent = '';
+ Polymer.dom(runningThreadEl).appendChild(threadLink);
+ }
+
+ root.querySelector('#args').object = cpuSlice.args;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-cpu-slice-sub-view',
+ tr.model.CpuSlice,
+ {
+ multi: false,
+ title: 'CPU Slice',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html
new file mode 100644
index 00000000000..ee47fe5c58a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_cpu_slice_sub_view_test.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_cpu_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBasicModel() {
+ const lines = [
+ 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('cpuSliceView_withCpuSliceOnExistingThread', function() {
+ const m = createBasicModel();
+
+ const cpu = m.kernel.cpus[1];
+ assert.isDefined(cpu);
+ const cpuSlice = cpu.slices[0];
+ assert.strictEqual('Binder_1', cpuSlice.title);
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+ assert.isDefined(thread);
+ assert.strictEqual(cpuSlice.threadThatWasRunning, thread);
+
+ const view = document.createElement('tr-ui-a-single-cpu-slice-sub-view');
+ const selection = new tr.model.EventSet();
+ selection.push(cpuSlice);
+ view.selection = selection;
+ this.addHTMLOutput(view);
+
+ // Clicking the analysis link should focus the Binder1's timeslice.
+ let didSelectionChangeHappen = false;
+ view.addEventListener('requestSelectionChange', function(e) {
+ assert.isTrue(e.selection.equals(
+ new tr.model.EventSet(thread.timeSlices[0])));
+ didSelectionChangeHappen = true;
+ });
+ Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click();
+ assert.isTrue(didSelectionChangeHappen);
+ });
+
+ test('cpuSliceViewWithCpuSliceOnMissingThread', function() {
+ const m = createBasicModel();
+
+ const cpu = m.kernel.cpus[1];
+ assert.isDefined(cpu);
+ const cpuSlice = cpu.slices[1];
+ assert.strictEqual('Android.launcher', cpuSlice.title);
+ assert.isUndefined(cpuSlice.thread);
+
+ const selection = new tr.model.EventSet();
+ selection.push(cpuSlice);
+
+ const view = document.createElement('tr-ui-a-single-cpu-slice-sub-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html
new file mode 100644
index 00000000000..d49125af9e3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view.html
@@ -0,0 +1,356 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/stack_frame.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex: 0 1;
+ flex-direction: column;
+ }
+ #table {
+ flex: 0 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table">
+ </tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ properties: {
+ isFlow: {
+ type: Boolean,
+ value: false
+ }
+ },
+
+ ready() {
+ this.currentSelection_ = undefined;
+ this.$.table.tableColumns = [
+ {
+ title: 'Label',
+ value(row) { return row.name; },
+ width: '150px'
+ },
+ {
+ title: 'Value',
+ width: '100%',
+ value(row) { return row.value; }
+ }
+ ];
+ this.$.table.showHeader = false;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ if (selection.length !== 1) {
+ throw new Error('Only supports single slices');
+ }
+ this.setSelectionWithoutErrorChecks(selection);
+ },
+
+ setSelectionWithoutErrorChecks(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ getFlowEventRows_(event) {
+ // TODO(nduca): Figure out if there is a cleaner way to do this.
+
+ const rows = this.getEventRowsHelper_(event);
+
+ // Put the ID up top.
+ rows.splice(0, 0, {
+ name: 'ID',
+ value: event.id
+ });
+
+ function createLinkTo(slice) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(function() {
+ return new tr.model.EventSet(slice);
+ });
+ Polymer.dom(linkEl).textContent = slice.userFriendlyName;
+ return linkEl;
+ }
+
+ rows.push({
+ name: 'From',
+ value: createLinkTo(event.startSlice)
+ });
+ rows.push({
+ name: 'To',
+ value: createLinkTo(event.endSlice)
+ });
+ return rows;
+ },
+
+ getEventRowsHelper_(event) {
+ const rows = [];
+
+ if (event.error) {
+ rows.push({ name: 'Error', value: event.error });
+ }
+
+ if (event.title) {
+ let title = event.title;
+ if (tr.isExported('tr-ui-e-chrome-codesearch')) {
+ const container = document.createElement('div');
+ container.appendChild(document.createTextNode(title));
+ const link = document.createElement('tr-ui-e-chrome-codesearch');
+ link.searchPhrase = title;
+ container.appendChild(link);
+ title = container;
+ }
+ rows.push({ name: 'Title', value: title });
+ }
+
+ if (event.category) {
+ rows.push({ name: 'Category', value: event.category });
+ }
+
+ if (event.model !== undefined) {
+ const ufc = event.model.getUserFriendlyCategoryFromEvent(event);
+ if (ufc !== undefined) {
+ rows.push({ name: 'User Friendly Category', value: ufc });
+ }
+ }
+
+ if (event.name) {
+ rows.push({ name: 'Name', value: event.name });
+ }
+
+ rows.push({
+ name: 'Start',
+ value: tr.v.ui.createScalarSpan(event.start, {
+ unit: tr.b.Unit.byName.timeStampInMs
+ })
+ });
+
+ if (event.duration) {
+ rows.push({
+ name: 'Wall Duration',
+ value: tr.v.ui.createScalarSpan(event.duration, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+
+ if (event.cpuDuration) {
+ rows.push({
+ name: 'CPU Duration',
+ value: tr.v.ui.createScalarSpan(event.cpuDuration, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+
+ if (event.subSlices !== undefined && event.subSlices.length !== 0) {
+ if (event.selfTime) {
+ rows.push({
+ name: 'Self Time',
+ value: tr.v.ui.createScalarSpan(event.selfTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+
+ if (event.cpuSelfTime) {
+ const cpuSelfTimeEl = tr.v.ui.createScalarSpan(event.cpuSelfTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ });
+ if (event.cpuSelfTime > event.selfTime) {
+ cpuSelfTimeEl.warning =
+ ' Note that CPU Self Time is larger than Self Time. ' +
+ 'This is a known limitation of this system, which occurs ' +
+ 'due to several subslices, rounding issues, and imprecise ' +
+ 'time at which we get cpu- and real-time.';
+ }
+ rows.push({ name: 'CPU Self Time', value: cpuSelfTimeEl });
+ }
+ }
+
+ if (event.durationInUserTime) {
+ rows.push({
+ name: 'Duration (U)',
+ value: tr.v.ui.createScalarSpan(event.durationInUserTime, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+
+ function createStackFrameEl(sf) {
+ const sfEl = document.createElement('tr-ui-a-stack-frame');
+ sfEl.stackFrame = sf;
+ return sfEl;
+ }
+ if (event.startStackFrame && event.endStackFrame) {
+ if (event.startStackFrame === event.endStackFrame) {
+ rows.push({name: 'Start+End Stack Trace',
+ value: createStackFrameEl(event.startStackFrame)});
+ } else {
+ rows.push({ name: 'Start Stack Trace',
+ value: createStackFrameEl(event.startStackFrame)});
+ rows.push({ name: 'End Stack Trace',
+ value: createStackFrameEl(event.endStackFrame)});
+ }
+ } else if (event.startStackFrame) {
+ rows.push({ name: 'Start Stack Trace',
+ value: createStackFrameEl(event.startStackFrame)});
+ } else if (event.endStackFrame) {
+ rows.push({ name: 'End Stack Trace',
+ value: createStackFrameEl(event.endStackFrame)});
+ }
+
+ if (event.info) {
+ const descriptionEl = tr.ui.b.createDiv({
+ textContent: event.info.description,
+ maxWidth: '300px'
+ });
+ rows.push({
+ name: 'Description',
+ value: descriptionEl
+ });
+
+
+ if (event.info.docLinks) {
+ event.info.docLinks.forEach(function(linkObject) {
+ const linkEl = document.createElement('a');
+ linkEl.target = '_blank';
+ linkEl.href = linkObject.href;
+ Polymer.dom(linkEl).textContent = Polymer.dom(linkObject).textContent;
+ rows.push({
+ name: linkObject.label,
+ value: linkEl
+ });
+ });
+ }
+ }
+
+ if (event.associatedAlerts.length) {
+ const alertSubRows = [];
+ event.associatedAlerts.forEach(function(alert) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(function() {
+ return new tr.model.EventSet(alert);
+ }, alert.info.description);
+ alertSubRows.push({
+ name: alert.title,
+ value: linkEl
+ });
+ });
+
+ rows.push({
+ name: 'Alerts', value: '',
+ isExpanded: true, subRows: alertSubRows
+ });
+ }
+ return rows;
+ },
+
+ getEventRows_(event) {
+ if (this.isFlow) {
+ return this.getFlowEventRows_(event);
+ }
+
+ return this.getEventRowsHelper_(event);
+ },
+
+ addArgsToRows_(rows, args) {
+ let n = 0;
+ for (const argName in args) {
+ n += 1;
+ }
+ if (n > 0) {
+ const subRows = [];
+ for (const argName in args) {
+ n += 1;
+ }
+ if (n > 0) {
+ const subRows = [];
+ for (const argName in args) {
+ const argView =
+ document.createElement('tr-ui-a-generic-object-view');
+ argView.object = args[argName];
+ subRows.push({name: argName, value: argView});
+ }
+ rows.push({
+ name: 'Args',
+ value: '',
+ isExpanded: true,
+ subRows
+ });
+ }
+ }
+ },
+
+ addContextsToRows_(rows, contexts) {
+ if (contexts.length) {
+ const subRows = contexts.map(function(context) {
+ const contextView =
+ document.createElement('tr-ui-a-generic-object-view');
+ contextView.object = context;
+ return {name: 'Context', value: contextView};
+ });
+ rows.push({
+ name: 'Contexts',
+ value: '',
+ isExpanded: true,
+ subRows
+ });
+ }
+ },
+
+ updateContents_() {
+ if (this.currentSelection_ === undefined) {
+ this.$.table.rows = [];
+ this.$.table.rebuild();
+ return;
+ }
+
+ const event = tr.b.getOnlyElement(this.currentSelection_);
+
+ const rows = this.getEventRows_(event);
+ if (event.argsStripped) {
+ rows.push({ name: 'Args', value: 'Stripped' });
+ } else {
+ this.addArgsToRows_(rows, event.args);
+ }
+ this.addContextsToRows_(rows, event.contexts);
+
+ const customizeRowsEvent = new tr.b.Event('customize-rows');
+ customizeRowsEvent.rows = rows;
+ this.dispatchEvent(customizeRowsEvent);
+
+ this.$.table.tableRows = rows;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html
new file mode 100644
index 00000000000..41c42308e3e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_event_sub_view_test.html
@@ -0,0 +1,277 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const Thread = tr.model.Thread;
+ const EventSet = tr.model.EventSet;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createSelection(customizeThreadCallback) {
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ customizeModelCallback(model) {
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ customizeThreadCallback(t53, model);
+ }
+ });
+
+ const t53 = model.processes[52].threads[53];
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+ assert.strictEqual(selection.length, 1);
+
+ return selection;
+ }
+
+ function createSelectionWithSingleSlice(opt_options) {
+ const options = opt_options || {};
+ return createSelection(function(t53, model) {
+ let fA;
+ let fB;
+ if (options.withStartStackFrame || options.withEndStackFrame) {
+ fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2']);
+ fB = tr.c.TestUtils.newStackTrace(model, ['b1', 'b2']);
+ }
+
+ const slice = newSliceEx({title: 'b', start: 0, duration: 0.002});
+ slice.category = options.withCategory ? 'foo' : '';
+
+ if (options.withStartStackFrame) {
+ slice.startStackFrame = options.withStartStackFrame === 'a' ? fA : fB;
+ }
+
+ if (options.withEndStackFrame) {
+ slice.endStackFrame = options.withEndStackFrame === 'a' ? fA : fB;
+ }
+
+ t53.sliceGroup.pushSlice(slice);
+ });
+ }
+
+ test('instantiate_withSingleSlice', function() {
+ const selection = createSelectionWithSingleSlice();
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+
+ test('alerts', function() {
+ const slice = newSliceEx({title: 'b', start: 0, duration: 0.002});
+
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'Critical alert');
+
+ const alert = new tr.model.Alert(ALERT_INFO_1, 5, [slice]);
+
+ const selection = new EventSet();
+ selection.push(slice);
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+
+ test('instantiate_withSingleSliceWithArg', function() {
+ const selection = createSelection(function(t53) {
+ const slice = newSliceEx({title: 'my_slice', start: 0, duration: 1.0});
+ slice.args = {
+ 'complex': {
+ 'b': '2 as a string',
+ 'c': [3, 4, 5]
+ }
+ };
+ t53.sliceGroup.pushSlice(slice);
+ });
+
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+
+ const gov = tr.ui.b.findDeepElementMatching(subView,
+ 'tr-ui-a-generic-object-view');
+ assert.isDefined(gov);
+ });
+
+
+ test('instantiate_withSingleSliceCategory', function() {
+ const selection = createSelectionWithSingleSlice({withCategory: true});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+
+ test('instantiate_withSingleStartStackFrame', function() {
+ const selection = createSelectionWithSingleSlice(
+ {withStartStackFrame: 'a'});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+
+ const e = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /Start Stack Trace/);
+ assert.isDefined(e);
+ assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame);
+ });
+
+ test('instantiate_withSingleEndStackFrame', function() {
+ const selection = createSelectionWithSingleSlice(
+ {withEndStackFrame: 'b'});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+
+ const e = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /End Stack Trace/);
+ assert.isDefined(e);
+ assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame);
+ assert.strictEqual(
+ Polymer.dom(e).nextSibling.children[0].stackFrame.title, 'b2');
+ });
+
+ test('instantiate_withDifferentStartAndEndStackFrames', function() {
+ const selection = createSelectionWithSingleSlice(
+ {withStartStackFrame: 'a',
+ withEndStackFrame: 'b'});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+
+ const eA = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /Start Stack Trace/);
+ assert.isDefined(eA);
+ assert.isDefined(Polymer.dom(eA).nextSibling.children[0].stackFrame);
+ assert.strictEqual(
+ Polymer.dom(eA).nextSibling.children[0].stackFrame.title, 'a2');
+
+ const eB = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /End Stack Trace/);
+ assert.isDefined(eB);
+ assert.isDefined(Polymer.dom(eB).nextSibling.children[0].stackFrame);
+ assert.strictEqual(
+ Polymer.dom(eB).nextSibling.children[0].stackFrame.title, 'b2');
+ });
+
+ test('instantiate_withSameStartAndEndStackFrames', function() {
+ const selection = createSelectionWithSingleSlice(
+ {withStartStackFrame: 'a',
+ withEndStackFrame: 'a'});
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+
+ const e = tr.ui.b.findDeepElementWithTextContent(
+ analysisEl, /Start\+End Stack Trace/);
+ assert.isDefined(e);
+ assert.isDefined(Polymer.dom(e).nextSibling.children[0].stackFrame);
+ assert.strictEqual(
+ Polymer.dom(e).nextSibling.children[0].stackFrame.title, 'a2');
+ });
+
+ test('analyzeSelectionWithSingleSlice', function() {
+ const selection = createSelectionWithSingleSlice();
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ subView, 'tr-ui-b-table');
+ assert.strictEqual(table.tableRows.length, 3);
+ if (tr.isExported('tr-ui-e-chrome-codesearch')) {
+ assert.strictEqual(table.tableRows[0].value.innerText, 'b');
+ } else {
+ assert.strictEqual(table.tableRows[0].value, 'b');
+ }
+ assert.strictEqual(table.tableRows[1].value.value, 0);
+ assert.strictEqual(table.tableRows[1].value.unit,
+ tr.b.Unit.byName.timeStampInMs);
+ assert.strictEqual(table.tableRows[2].value.value, 0.002);
+ assert.strictEqual(table.tableRows[2].value.unit,
+ tr.b.Unit.byName.timeDurationInMs);
+ });
+
+ test('analyzeSelectionWithSingleSliceCategory', function() {
+ const selection = createSelectionWithSingleSlice({withCategory: true});
+
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ subView, 'tr-ui-b-table');
+ assert.strictEqual(table.tableRows.length, 4);
+ if (tr.isExported('tr-ui-e-chrome-codesearch')) {
+ assert.strictEqual(table.tableRows[0].value.innerText, 'b');
+ } else {
+ assert.strictEqual(table.tableRows[0].value, 'b');
+ }
+ assert.strictEqual(table.tableRows[1].value, 'foo');
+ assert.strictEqual(table.tableRows[2].value.value, 0);
+ assert.strictEqual(table.tableRows[2].value.unit,
+ tr.b.Unit.byName.timeStampInMs);
+ assert.strictEqual(table.tableRows[3].value.value, 0.002);
+ assert.strictEqual(table.tableRows[3].value.unit,
+ tr.b.Unit.byName.timeDurationInMs);
+ });
+
+ test('instantiate_withSingleSliceContainingIDRef', function() {
+ const model = new Model();
+ const p1 = model.getOrCreateProcess(1);
+ const myObjectSlice = p1.objects.addSnapshot(
+ '0x1000', 'cat', 'my_object', 0);
+
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(newSliceEx({title: 'b', start: 0, duration: 2}));
+ t1.sliceGroup.slices[0].args.my_object = myObjectSlice;
+
+ const t1track = {};
+ t1track.thread = t1;
+
+ const selection = new EventSet();
+ selection.push(t1.sliceGroup.slices[0]);
+ assert.strictEqual(selection.length, 1);
+
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+
+ const analysisLink = tr.ui.b.findDeepElementMatching(subView,
+ 'tr-ui-a-analysis-link');
+ assert.isDefined(analysisLink);
+ });
+
+ test('instantiate_withSingleSliceContainingInfo', function() {
+ const slice = newSliceEx({title: 'b', start: 0, duration: 1});
+ slice.info = new tr.model.EventInfo(
+ 'Info title', 'Description');
+
+ const selection = new EventSet();
+ selection.push(slice);
+
+ const analysisEl = document.createElement('tr-ui-a-single-event-sub-view');
+ analysisEl.selection = selection;
+ this.addHTMLOutput(analysisEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html
new file mode 100644
index 00000000000..b201b161ffd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id="tr-ui-a-single-flow-event-sub-view">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ </style>
+ <tr-ui-a-single-event-sub-view id="singleEventSubView">
+ </tr-ui-a-single-event-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+function createAnalysisLinkTo(event) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(
+ new tr.model.EventSet(event), event.userFriendlyName);
+ return linkEl;
+}
+
+Polymer({
+ is: 'tr-ui-a-single-flow-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ listeners: {
+ 'singleEventSubView.customize-rows': 'onCustomizeRows_'
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.$.singleEventSubView.setSelectionWithoutErrorChecks(selection);
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ /**
+ * Event handler for an event that's fired after the single event sub view has
+ * finished row construction. This hook gives us the opportunity to customize
+ * the rows present in the sub view.
+ */
+ onCustomizeRows_(e) {
+ const event = tr.b.getOnlyElement(this.currentSelection_);
+ const rows = e.rows;
+
+ rows.unshift({
+ name: 'ID',
+ value: event.id
+ });
+ rows.push({
+ name: 'From',
+ value: createAnalysisLinkTo(event.startSlice)
+ });
+ rows.push({
+ name: 'To',
+ value: createAnalysisLinkTo(event.endSlice)
+ });
+ }
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-flow-event-sub-view',
+ tr.model.FlowEvent,
+ {
+ multi: false,
+ title: 'Flow Event',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html
new file mode 100644
index 00000000000..31e3eb18f25
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_flow_event_sub_view_test.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+ const TestUtils = tr.c.TestUtils;
+
+ test('analyzeSelectionWithSingleEvent', function() {
+ const model = TestUtils.newModel(function(model) {
+ model.p1 = model.getOrCreateProcess(1);
+ model.t2 = model.p1.getOrCreateThread(model.p1);
+ model.sA = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ title: 'a', start: 0, end: 2
+ }));
+ model.sB = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({
+ title: 'b', start: 9, end: 11
+ }));
+ model.fe = TestUtils.newFlowEventEx({
+ cat: 'cat',
+ id: 1234,
+ title: 'MyFlow',
+ start: 1,
+ end: 10,
+ startSlice: model.sA,
+ endSlice: model.sB
+ });
+ model.flowEvents.push(model.fe);
+ });
+
+ const selection = new EventSet();
+ selection.push(model.fe);
+ assert.strictEqual(selection.length, 1);
+
+ const subView = document.createElement('tr-ui-a-single-event-sub-view');
+ subView.isFlow = true;
+ subView.selection = selection;
+ this.addHTMLOutput(subView);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html
new file mode 100644
index 00000000000..e89fa2626ef
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_frame_sub_view.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+
+<dom-module id='tr-ui-a-single-frame-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #asv {
+ flex: 0 0 auto;
+ align-self: stretch;
+ }
+ </style>
+ <tr-ui-a-alert-sub-view id="asv">
+ </tr-ui-a-alert-sub-view>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-frame-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.$.asv.selection = tr.b.getOnlyElement(selection).associatedAlerts;
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+ return tr.b.getOnlyElement(this.currentSelection_).associatedEvents;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-frame-sub-view',
+ tr.model.Frame,
+ {
+ multi: false,
+ title: 'Frame',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html
new file mode 100644
index 00000000000..43b0e8a80cd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id='tr-ui-a-single-instant-event-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ </style>
+ <div id='content'></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-instant-event-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ set selection(selection) {
+ Polymer.dom(this.$.content).textContent = '';
+ const realView = document.createElement('tr-ui-a-single-event-sub-view');
+ realView.setSelectionWithoutErrorChecks(selection);
+
+ Polymer.dom(this.$.content).appendChild(realView);
+
+ this.currentSelection_ = selection;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-instant-event-sub-view',
+ tr.model.InstantEvent,
+ {
+ multi: false,
+ title: 'Instant Event',
+ });
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-multi-instant-event-sub-view',
+ tr.model.InstantEvent,
+ {
+ multi: true,
+ title: 'Instant Events',
+ });
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html
new file mode 100644
index 00000000000..4ad85d2e6db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_instant_event_sub_view_test.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const Thread = tr.model.Thread;
+ const EventSet = tr.model.EventSet;
+
+ test('analyzeSelectionWithSingleEvent', function() {
+ const model = new Model();
+ const p52 = model.getOrCreateProcess(52);
+ const t53 = p52.getOrCreateThread(53);
+
+ const ie = new tr.model.ProcessInstantEvent('cat', 'title', 7, 10, {});
+ ie.duration = 20;
+ p52.instantEvents.push(ie);
+
+
+ const selection = new EventSet();
+ selection.push(ie);
+ assert.strictEqual(selection.length, 1);
+
+ const subView = document.createElement(
+ 'tr-ui-a-single-instant-event-sub-view');
+ subView.selection = selection;
+
+ this.addHTMLOutput(subView);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html
new file mode 100644
index 00000000000..49810ab3fbd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_instance_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id='tr-ui-a-single-object-instance-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+
+ #snapshots > * {
+ display: block;
+ }
+
+ :host {
+ overflow: auto;
+ display: block;
+ }
+
+ * {
+ -webkit-user-select: text;
+ }
+
+ .title {
+ border-bottom: 1px solid rgb(128, 128, 128);
+ font-size: 110%;
+ font-weight: bold;
+ }
+
+ td, th {
+ font-family: monospace;
+ vertical-align: top;
+ }
+ </style>
+ <div id='content'></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-object-instance-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get requiresTallView() {
+ if (this.$.content.children.length === 0) {
+ return false;
+ }
+ if (this.$.content.children[0] instanceof
+ tr.ui.analysis.ObjectInstanceView) {
+ return this.$.content.children[0].requiresTallView;
+ }
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ const instance = tr.b.getOnlyElement(selection);
+ if (!(instance instanceof tr.model.ObjectInstance)) {
+ throw new Error('Only supports object instances');
+ }
+
+ Polymer.dom(this.$.content).textContent = '';
+ this.currentSelection_ = selection;
+
+ const typeInfo = tr.ui.analysis.ObjectInstanceView.getTypeInfo(
+ instance.category, instance.typeName);
+ if (typeInfo) {
+ const customView = new typeInfo.constructor();
+ Polymer.dom(this.$.content).appendChild(customView);
+ customView.modelEvent = instance;
+ } else {
+ this.appendGenericAnalysis_(instance);
+ }
+ },
+
+ appendGenericAnalysis_(instance) {
+ let html = '';
+ html += '<div class="title">' +
+ instance.typeName + ' ' +
+ instance.id + '</div>\n';
+ html += '<table>';
+ html += '<tr>';
+ html += '<tr><td>creationTs:</td><td>' +
+ instance.creationTs + '</td></tr>\n';
+ if (instance.deletionTs !== Number.MAX_VALUE) {
+ html += '<tr><td>deletionTs:</td><td>' +
+ instance.deletionTs + '</td></tr>\n';
+ } else {
+ html += '<tr><td>deletionTs:</td><td>not deleted</td></tr>\n';
+ }
+ html += '<tr><td>snapshots:</td><td id="snapshots"></td></tr>\n';
+ html += '</table>';
+ Polymer.dom(this.$.content).innerHTML = html;
+ const snapshotsEl = Polymer.dom(this.$.content).querySelector('#snapshots');
+ instance.snapshots.forEach(function(snapshot) {
+ const snapshotLink = document.createElement('tr-ui-a-analysis-link');
+ snapshotLink.selection = new tr.model.EventSet(snapshot);
+ Polymer.dom(snapshotsEl).appendChild(snapshotLink);
+ });
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-object-instance-sub-view',
+ tr.model.ObjectInstance,
+ {
+ multi: false,
+ title: 'Object Instance',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html
new file mode 100644
index 00000000000..f5414dd957a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_instance_sub_view_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_object_instance_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ObjectInstance = tr.model.ObjectInstance;
+
+ test('analyzeSelectionWithObjectInstanceUnknownType', function() {
+ const i10 = new ObjectInstance(
+ {}, '0x1000', 'cat', 'someUnhandledName', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+ const s20 = i10.addSnapshot(20, {foo: 2});
+
+ const selection = new tr.model.EventSet();
+ selection.push(i10);
+
+ const view =
+ document.createElement('tr-ui-a-single-object-instance-sub-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html
new file mode 100644
index 00000000000..5565db8d004
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_instance_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-object-snapshot-sub-view'>
+ <template>
+ <style>
+ #args {
+ white-space: pre;
+ }
+
+ :host {
+ overflow: auto;
+ display: flex;
+ }
+
+ ::content * {
+ -webkit-user-select: text;
+ }
+
+ ::content .title {
+ border-bottom: 1px solid rgb(128, 128, 128);
+ font-size: 110%;
+ font-weight: bold;
+ }
+
+ ::content td, th {
+ font-family: monospace;
+ vertical-align: top;
+ }
+ </style>
+ <slot></slot>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-object-snapshot-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get requiresTallView() {
+ if (this.children.length === 0) {
+ return false;
+ }
+ if (this.children[0] instanceof tr.ui.analysis.ObjectSnapshotView) {
+ return this.children[0].requiresTallView;
+ }
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ const snapshot = tr.b.getOnlyElement(selection);
+ if (!(snapshot instanceof tr.model.ObjectSnapshot)) {
+ throw new Error('Only supports object instances');
+ }
+
+ Polymer.dom(this).textContent = '';
+ this.currentSelection_ = selection;
+
+ const typeInfo = tr.ui.analysis.ObjectSnapshotView.getTypeInfo(
+ snapshot.objectInstance.category, snapshot.objectInstance.typeName);
+ if (typeInfo) {
+ const customView = new typeInfo.constructor();
+ Polymer.dom(this).appendChild(customView);
+ customView.modelEvent = snapshot;
+ } else {
+ this.appendGenericAnalysis_(snapshot);
+ }
+ },
+
+ appendGenericAnalysis_(snapshot) {
+ const instance = snapshot.objectInstance;
+
+ Polymer.dom(this).textContent = '';
+
+ const titleEl = document.createElement('div');
+ Polymer.dom(titleEl).classList.add('title');
+ Polymer.dom(titleEl).appendChild(document.createTextNode('Snapshot of '));
+ Polymer.dom(this).appendChild(titleEl);
+
+ const instanceLinkEl = document.createElement('tr-ui-a-analysis-link');
+ instanceLinkEl.selection = new tr.model.EventSet(instance);
+ Polymer.dom(titleEl).appendChild(instanceLinkEl);
+
+ Polymer.dom(titleEl).appendChild(document.createTextNode(' @ '));
+
+ Polymer.dom(titleEl).appendChild(tr.v.ui.createScalarSpan(snapshot.ts, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument,
+ inline: true,
+ }));
+
+ const tableEl = document.createElement('table');
+ Polymer.dom(this).appendChild(tableEl);
+
+ const rowEl = document.createElement('tr');
+ Polymer.dom(tableEl).appendChild(rowEl);
+
+ const labelEl = document.createElement('td');
+ Polymer.dom(labelEl).textContent = 'args:';
+ Polymer.dom(rowEl).appendChild(labelEl);
+
+ const argsEl = document.createElement('td');
+ argsEl.id = 'args';
+ Polymer.dom(rowEl).appendChild(argsEl);
+
+ const objectViewEl = document.createElement('tr-ui-a-generic-object-view');
+ objectViewEl.object = snapshot.args;
+ Polymer.dom(argsEl).appendChild(objectViewEl);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-object-snapshot-sub-view',
+ tr.model.ObjectSnapshot,
+ {
+ multi: false,
+ title: 'Object Snapshot',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html
new file mode 100644
index 00000000000..41fca173931
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_object_snapshot_sub_view_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_object_snapshot_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate_snapshotView', function() {
+ const i10 = new tr.model.ObjectInstance(
+ {}, '0x1000', 'cat', 'name', 10);
+ const s10 = i10.addSnapshot(10, {foo: 1});
+ i10.updateBounds();
+
+ const selection = new tr.model.EventSet();
+ selection.push(s10);
+
+ const view =
+ document.createElement('tr-ui-a-single-object-snapshot-sub-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html
new file mode 100644
index 00000000000..7396cfa3eca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-power-sample-table'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-power-sample-table',
+
+ ready() {
+ this.$.table.tableColumns = [
+ {
+ title: 'Time',
+ width: '100px',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.start, {
+ unit: tr.b.Unit.byName.timeStampInMs
+ });
+ }
+ },
+ {
+ title: 'Power',
+ width: '100%',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.powerInW, {
+ unit: tr.b.Unit.byName.powerInWatts
+ });
+ }
+ }
+ ];
+ this.sample = undefined;
+ },
+
+ get sample() {
+ return this.sample_;
+ },
+
+ set sample(sample) {
+ this.sample_ = sample;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ if (this.sample === undefined) {
+ this.$.table.tableRows = [];
+ } else {
+ this.$.table.tableRows = [this.sample];
+ }
+ this.$.table.rebuild();
+ }
+});
+</script>
+
+<dom-module id='tr-ui-a-single-power-sample-sub-view'>
+ <template>
+ <style>
+ :host { display: block; }
+ </style>
+ <tr-ui-a-power-sample-table id="samplesTable">
+ </tr-ui-a-power-sample-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-power-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ ready() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ if (this.selection.length !== 1) {
+ throw new Error('Cannot pass multiple samples to sample table.');
+ }
+ this.$.samplesTable.sample = tr.b.getOnlyElement(this.selection);
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-power-sample-sub-view',
+ tr.model.PowerSample,
+ {
+ multi: false,
+ title: 'Power Sample',
+ });
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html
new file mode 100644
index 00000000000..8ee1dfcf899
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_power_sample_sub_view_test.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/single_power_sample_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const series = new tr.model.PowerSeries(model.device);
+ series.addPowerSample(1, 1);
+
+ const view = document.createElement('tr-ui-a-single-power-sample-sub-view');
+ view.selection = new tr.model.EventSet(series.samples);
+
+ this.addHTMLOutput(view);
+ });
+
+ test('setSelection', function() {
+ const model = new tr.Model();
+ const series = new tr.model.PowerSeries(model.device);
+ series.addPowerSample(1, 1);
+
+ const view = document.createElement('tr-ui-a-single-power-sample-sub-view');
+ const eventSet = new tr.model.EventSet(series.samples);
+ view.selection = eventSet;
+
+ assert.deepEqual(view.$.samplesTable.sample,
+ tr.b.getOnlyElement(series.samples));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html
new file mode 100644
index 00000000000..851c60952ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/stack_frame.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-sample-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="content"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-sample-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ ready() {
+ this.$.content.tableColumns = [
+ {
+ title: '',
+ value: row => row.title,
+ width: '100px'
+ },
+ {
+ title: '',
+ value: row => row.value,
+ width: '100%'
+ }
+ ];
+ this.$.content.showHeader = false;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+
+ if (this.currentSelection_ === undefined) {
+ this.$.content.tableRows = [];
+ return;
+ }
+
+ const sample = tr.b.getOnlyElement(this.currentSelection_);
+ const table = this.$.content;
+ const rows = [];
+
+ rows.push({
+ title: 'Title',
+ value: sample.title
+ });
+
+ rows.push({
+ title: 'Sample time',
+ value: tr.v.ui.createScalarSpan(sample.start, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument
+ })
+ });
+
+ const callStackTableEl = document.createElement('tr-ui-b-table');
+ callStackTableEl.tableRows = sample.getNodesAsArray().reverse();
+ callStackTableEl.tableColumns = [
+ {
+ title: 'function name',
+ value: row => row.functionName || '(anonymous function)'
+ },
+ {
+ title: 'location',
+ value: row => row.url
+ }
+ ];
+ callStackTableEl.rebuild();
+ rows.push({
+ title: 'Call stack',
+ value: callStackTableEl
+ });
+ table.tableRows = rows;
+ table.rebuild();
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-sample-sub-view',
+ tr.model.Sample,
+ {
+ multi: false,
+ title: 'Sample',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html
new file mode 100644
index 00000000000..7f8c131f82c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_sample_sub_view_test.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const EventSet = tr.model.EventSet;
+ const newSampleNamed = tr.c.TestUtils.newSampleNamed;
+
+ test('instantiate_withSingleSample', function() {
+ let t53;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ model.samples.push(newSampleNamed(t53, 'X', 'my-category',
+ ['a', 'b', 'c'], 0.184));
+ }
+ });
+
+ const t53track = {};
+ t53track.thread = t53;
+
+ const selection = new EventSet();
+
+ assert.strictEqual(selection.length, 0);
+ selection.push(t53.samples[0]);
+ assert.strictEqual(selection.length, 1);
+
+ const view = document.createElement('tr-ui-a-single-sample-sub-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+
+ const table = tr.ui.b.findDeepElementMatching(
+ view, 'tr-ui-b-table');
+
+ const rows = table.tableRows;
+ assert.strictEqual(rows.length, 3);
+ assert.strictEqual(rows[0].value, 'X');
+ assert.strictEqual(rows[1].value.value, 0.184);
+ assert.strictEqual(rows[1].value.unit, tr.b.Unit.byName.timeStampInMs);
+
+ const callStackRows = rows[2].value.tableRows;
+ assert.lengthOf(callStackRows, 3);
+ assert.deepEqual(callStackRows.map(x => x.title), ['a', 'b', 'c']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html
new file mode 100644
index 00000000000..720fdfeb65a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/related_events.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+
+<dom-module id='tr-ui-a-single-thread-slice-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ }
+ #events {
+ display: flex;
+ flex-direction: column;
+ }
+
+ </style>
+ <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view>
+ <div id="events">
+ <tr-ui-a-related-events id="relatedEvents">
+ </tr-ui-a-related-events>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.selection = selection;
+ this.$.relatedEvents.setRelatedEvents(selection);
+ if (this.$.relatedEvents.hasRelatedEvents()) {
+ this.$.relatedEvents.style.display = '';
+ } else {
+ this.$.relatedEvents.style.display = 'none';
+ }
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-thread-slice-sub-view',
+ tr.model.ThreadSlice,
+ {
+ multi: false,
+ title: 'Slice',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html
new file mode 100644
index 00000000000..84bb292384e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_slice_sub_view_test.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/analysis/single_thread_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const newFlowEventEx = tr.c.TestUtils.newFlowEventEx;
+
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+ t53.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0.0, duration: 0.5}));
+ t53.sliceGroup.createSubSlices();
+
+ const selection = new tr.model.EventSet();
+ selection.push(t53.sliceGroup.slices[0]);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-single-thread-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('instantiateWithFlowEvent', function() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+
+ m.t2 = m.p1.getOrCreateThread(2);
+ m.t3 = m.p1.getOrCreateThread(3);
+ m.t4 = m.p1.getOrCreateThread(4);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5,
+ type: tr.model.ThreadSlice}));
+ m.sB = m.t3.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15,
+ type: tr.model.ThreadSlice}));
+ m.sC = m.t4.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20,
+ type: tr.model.ThreadSlice}));
+
+ m.t2.createSubSlices();
+ m.t3.createSubSlices();
+ m.t4.createSubSlices();
+
+ m.f1 = newFlowEventEx({
+ title: 'flowish', start: 0, end: 10,
+ startSlice: m.sA,
+ endSlice: m.sB
+ });
+ m.f2 = newFlowEventEx({
+ title: 'flowish', start: 15, end: 21,
+ startSlice: m.sB,
+ endSlice: m.sC
+ });
+ });
+
+ const selection = new tr.model.EventSet();
+ selection.push(m.sA);
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-single-thread-slice-sub-view');
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html
new file mode 100644
index 00000000000..225b2729769
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view.html
@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-thread-time-slice-sub-view'>
+ <template>
+ <style>
+ table {
+ border-collapse: collapse;
+ border-width: 0;
+ margin-bottom: 25px;
+ width: 100%;
+ }
+
+ table tr > td:first-child {
+ padding-left: 2px;
+ }
+
+ table tr > td {
+ padding: 2px 4px 2px 4px;
+ vertical-align: text-top;
+ width: 150px;
+ }
+
+ table td td {
+ padding: 0 0 0 0;
+ width: auto;
+ }
+ tr {
+ vertical-align: top;
+ }
+
+ tr:nth-child(2n+0) {
+ background-color: #e2e2e2;
+ }
+ </style>
+ <table>
+ <tr>
+ <td>Running process:</td><td id="process-name"></td>
+ </tr>
+ <tr>
+ <td>Running thread:</td><td id="thread-name"></td>
+ </tr>
+ <tr>
+ <td>State:</td>
+ <td><b><span id="state"></span></b></td>
+ </tr>
+ <tr>
+ <td>Start:</td>
+ <td>
+ <tr-v-ui-scalar-span id="start">
+ </tr-v-ui-scalar-span>
+ </td>
+ </tr>
+ <tr>
+ <td>Duration:</td>
+ <td>
+ <tr-v-ui-scalar-span id="duration">
+ </tr-v-ui-scalar-span>
+ </td>
+ </tr>
+
+ <tr>
+ <td>On CPU:</td><td id="on-cpu"></td>
+ </tr>
+
+ <tr>
+ <td>Running instead:</td><td id="running-instead"></td>
+ </tr>
+
+ <tr>
+ <td>Args:</td><td id="args"></td>
+ </tr>
+ </table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-thread-time-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ const timeSlice = tr.b.getOnlyElement(selection);
+
+ if (!(timeSlice instanceof tr.model.ThreadTimeSlice)) {
+ throw new Error('Only supports thread time slices');
+ }
+
+ this.currentSelection_ = selection;
+
+ const thread = timeSlice.thread;
+
+ const root = Polymer.dom(this.root);
+ Polymer.dom(root.querySelector('#state')).textContent =
+ timeSlice.title;
+ const stateColor = tr.b.ColorScheme.colorsAsStrings[timeSlice.colorId];
+ root.querySelector('#state').style.backgroundColor = stateColor;
+
+ Polymer.dom(root.querySelector('#process-name')).textContent =
+ thread.parent.userFriendlyName;
+ Polymer.dom(root.querySelector('#thread-name')).textContent =
+ thread.userFriendlyName;
+
+ root.querySelector('#start').setValueAndUnit(
+ timeSlice.start, tr.b.Unit.byName.timeStampInMs);
+ root.querySelector('#duration').setValueAndUnit(
+ timeSlice.duration, tr.b.Unit.byName.timeDurationInMs);
+
+ const onCpuEl = root.querySelector('#on-cpu');
+ Polymer.dom(onCpuEl).textContent = '';
+ const runningInsteadEl = root.querySelector('#running-instead');
+ if (timeSlice.cpuOnWhichThreadWasRunning) {
+ Polymer.dom(runningInsteadEl.parentElement).removeChild(runningInsteadEl);
+
+ const cpuLink = document.createElement('tr-ui-a-analysis-link');
+ cpuLink.selection = new tr.model.EventSet(
+ timeSlice.getAssociatedCpuSlice());
+ Polymer.dom(cpuLink).textContent =
+ timeSlice.cpuOnWhichThreadWasRunning.userFriendlyName;
+ Polymer.dom(onCpuEl).appendChild(cpuLink);
+ } else {
+ Polymer.dom(onCpuEl.parentElement).removeChild(onCpuEl);
+
+ const cpuSliceThatTookCpu = timeSlice.getCpuSliceThatTookCpu();
+ if (cpuSliceThatTookCpu) {
+ const cpuLink = document.createElement('tr-ui-a-analysis-link');
+ cpuLink.selection = new tr.model.EventSet(cpuSliceThatTookCpu);
+ if (cpuSliceThatTookCpu.thread) {
+ Polymer.dom(cpuLink).textContent =
+ cpuSliceThatTookCpu.thread.userFriendlyName;
+ } else {
+ Polymer.dom(cpuLink).textContent = cpuSliceThatTookCpu.title;
+ }
+ Polymer.dom(runningInsteadEl).appendChild(cpuLink);
+ } else {
+ Polymer.dom(runningInsteadEl.parentElement).removeChild(
+ runningInsteadEl);
+ }
+ }
+
+ const argsEl = root.querySelector('#args');
+ if (Object.keys(timeSlice.args).length > 0) {
+ const argsView =
+ document.createElement('tr-ui-a-generic-object-view');
+ argsView.object = timeSlice.args;
+
+ argsEl.parentElement.style.display = '';
+ Polymer.dom(argsEl).textContent = '';
+ Polymer.dom(argsEl).appendChild(argsView);
+ } else {
+ argsEl.parentElement.style.display = 'none';
+ }
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-thread-time-slice-sub-view',
+ tr.model.ThreadTimeSlice,
+ {
+ multi: false,
+ title: 'Thread Timeslice',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html
new file mode 100644
index 00000000000..bfffd41861b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_thread_time_slice_sub_view_test.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/single_thread_time_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createBasicModel() {
+ const lines = [
+ 'Android.launcher-584 [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+ 'Android.launcher-584 [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+ ' Binder_1-217 [001] ...1 12622.507175: tracing_mark_write: E',
+ ' Binder_1-217 [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+ ];
+
+ return tr.c.TestUtils.newModelWithEvents([lines.join('\n')], {
+ shiftWorldToZero: false
+ });
+ }
+
+ test('runningSlice', function() {
+ const m = createBasicModel();
+
+ const cpu = m.kernel.cpus[1];
+ const binderSlice = cpu.slices[0];
+ assert.strictEqual(binderSlice.title, 'Binder_1');
+ const launcherSlice = cpu.slices[1];
+ assert.strictEqual(launcherSlice.title, 'Android.launcher');
+
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+
+ const view = document.createElement(
+ 'tr-ui-a-single-thread-time-slice-sub-view');
+ const selection = new tr.model.EventSet();
+ selection.push(thread.timeSlices[0]);
+ view.selection = selection;
+ this.addHTMLOutput(view);
+
+ // Clicking the analysis link should focus the Binder1's timeslice.
+ let didSelectionChangeHappen = false;
+ view.addEventListener('requestSelectionChange', function(e) {
+ assert.isTrue(e.selection.equals(new tr.model.EventSet(binderSlice)));
+ didSelectionChangeHappen = true;
+ });
+ Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click();
+ assert.isTrue(didSelectionChangeHappen);
+ });
+
+ test('sleepingSlice', function() {
+ const m = createBasicModel();
+
+ const cpu = m.kernel.cpus[1];
+ const binderSlice = cpu.slices[0];
+ assert.strictEqual(binderSlice.title, 'Binder_1');
+ const launcherSlice = cpu.slices[1];
+ assert.strictEqual(launcherSlice.title, 'Android.launcher');
+
+
+ const thread = m.findAllThreadsNamed('Binder_1')[0];
+
+ const view = document.createElement(
+ 'tr-ui-a-single-thread-time-slice-sub-view');
+ const selection = new tr.model.EventSet();
+ selection.push(thread.timeSlices[1]);
+ view.selection = selection;
+ this.addHTMLOutput(view);
+
+ // Clicking the analysis link should focus the Android.launcher slice
+ let didSelectionChangeHappen = false;
+ view.addEventListener('requestSelectionChange', function(e) {
+ assert.isTrue(e.selection.equals(new tr.model.EventSet(launcherSlice)));
+ didSelectionChangeHappen = true;
+ });
+ Polymer.dom(view.root).querySelector('tr-ui-a-analysis-link').click();
+ assert.isTrue(didSelectionChangeHappen);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html
new file mode 100644
index 00000000000..76110b4b468
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/single_user_expectation_sub_view.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+<link rel="import"
+ href="/tracing/ui/analysis/user_expectation_related_samples_table.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-a-single-user-expectation-sub-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ }
+ #events {
+ display: flex;
+ flex-direction: column;
+ }
+ </style>
+ <tr-ui-a-single-event-sub-view id="realView"></tr-ui-a-single-event-sub-view>
+ <div id="events">
+ <tr-ui-a-user-expectation-related-samples-table id="relatedSamples"></tr-ui-a-user-expectation-related-samples-table>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-single-user-expectation-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ created() {
+ this.currentSelection_ = undefined;
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ set selection(selection) {
+ this.$.realView.addEventListener('customize-rows',
+ this.onCustomizeRows_.bind(this));
+
+ this.currentSelection_ = selection;
+ this.$.realView.setSelectionWithoutErrorChecks(selection);
+
+ this.$.relatedSamples.selection = selection;
+ if (this.$.relatedSamples.hasRelatedSamples()) {
+ this.$.events.style.display = '';
+ } else {
+ this.$.events.style.display = 'none';
+ }
+ },
+
+ get relatedEventsToHighlight() {
+ if (!this.currentSelection_) return undefined;
+ return tr.b.getOnlyElement(this.currentSelection_).associatedEvents;
+ },
+
+ onCustomizeRows_(event) {
+ const ue = tr.b.getOnlyElement(this.selection);
+
+ if (ue.rawCpuMs) {
+ event.rows.push({
+ name: 'Total CPU',
+ value: tr.v.ui.createScalarSpan(ue.totalCpuMs, {
+ unit: tr.b.Unit.byName.timeDurationInMs
+ })
+ });
+ }
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-single-user-expectation-sub-view',
+ tr.model.um.UserExpectation,
+ {
+ multi: false,
+ title: 'User Expectation',
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html
new file mode 100644
index 00000000000..92c4594af5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-stack-frame'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-stack-frame',
+
+ ready() {
+ this.stackFrame_ = undefined;
+ this.$.table.tableColumns = [];
+ this.$.table.showHeader = true;
+ },
+
+ get stackFrame() {
+ return this.stackFrame_;
+ },
+
+ set stackFrame(stackFrame) {
+ const table = this.$.table;
+
+ this.stackFrame_ = stackFrame;
+ if (stackFrame === undefined) {
+ table.tableColumns = [];
+ table.tableRows = [];
+ table.rebuild();
+ return;
+ }
+
+ let hasName = false;
+ let hasTitle = false;
+
+ table.tableRows = stackFrame.stackTrace;
+ table.tableRows.forEach(function(row) {
+ hasName |= row.name !== undefined;
+ hasTitle |= row.title !== undefined;
+ });
+
+ const cols = [];
+ if (hasName) {
+ cols.push({
+ title: 'Name',
+ value(row) { return row.name; }
+ });
+ }
+
+ if (hasTitle) {
+ cols.push({
+ title: 'Title',
+ value(row) { return row.title; }
+ });
+ }
+
+ table.tableColumns = cols;
+ table.rebuild();
+ },
+
+ tableForTesting() {
+ return this.$.table;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html
new file mode 100644
index 00000000000..4523906a321
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stack_frame_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/analysis/stack_frame.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const model = new tr.Model();
+ const fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2', 'a3']);
+
+ const stackFrameView = document.createElement('tr-ui-a-stack-frame');
+ stackFrameView.stackFrame = fA;
+ this.addHTMLOutput(stackFrameView);
+ });
+
+ test('clearingStackFrame', function() {
+ const model = new tr.Model();
+ const fA = tr.c.TestUtils.newStackTrace(model, ['a1', 'a2', 'a3']);
+
+ const stackFrameView = document.createElement('tr-ui-a-stack-frame');
+ stackFrameView.stackFrame = fA;
+ stackFrameView.stackFrame = undefined;
+
+ assert.isUndefined(stackFrameView.stackFrame);
+ assert.lengthOf(stackFrameView.$.table.$.body.children, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html
new file mode 100644
index 00000000000..0e4f633fb00
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/rebuildable_behavior.html">
+
+<!--
+@fileoverview Analysis view stacked pane. See the stacked pane view element
+(tr-ui-a-stacked-pane-view) documentation for more details.
+-->
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ const StackedPaneImpl = {
+ /**
+ * Request changing the child pane of this pane in the associated stacked
+ * pane view. If the assigned builder is undefined, request removing the
+ * current child pane.
+ *
+ * Note that setting this property before appended() is called will have no
+ * effect (as there will be no listener attached to the pane).
+ *
+ * This method is intended to be called by subclasses.
+ */
+ set childPaneBuilder(childPaneBuilder) {
+ this.childPaneBuilder_ = childPaneBuilder;
+ this.dispatchEvent(new tr.b.Event('request-child-pane-change'));
+ },
+
+ get childPaneBuilder() {
+ return this.childPaneBuilder_;
+ },
+
+ /**
+ * Called right after the pane is appended to a pane view.
+ *
+ * This method triggers an immediate rebuild by default. Subclasses are
+ * free to change this behavior (e.g. if a pane has lots of data to display,
+ * it might decide to defer rebuilding in order not to cause jank).
+ */
+ appended() {
+ this.rebuild();
+ }
+ };
+
+ const StackedPane = [tr.ui.analysis.RebuildableBehavior, StackedPaneImpl];
+
+ return {
+ StackedPane,
+ };
+});
+
+Polymer({
+ is: 'tr-ui-a-stacked-pane',
+ behaviors: [tr.ui.analysis.StackedPane]
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html
new file mode 100644
index 00000000000..7af70fa3d7a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_test.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('changeChildPane', function() {
+ const pane = document.createElement('tr-ui-a-stacked-pane');
+ let didFireEvent;
+ pane.addEventListener('request-child-pane-change', function() {
+ didFireEvent = true;
+ });
+
+ didFireEvent = false;
+ pane.childPaneBuilder = undefined;
+ assert.isTrue(didFireEvent);
+
+ didFireEvent = false;
+ pane.childPaneBuilder = function() {
+ return undefined;
+ };
+ assert.isTrue(didFireEvent);
+
+ didFireEvent = false;
+ pane.childPaneBuilder = function() {
+ return document.createElement('tr-ui-a-stacked-pane');
+ };
+ assert.isTrue(didFireEvent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html
new file mode 100644
index 00000000000..e8bea2dd034
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<!--
+@fileoverview Analysis view container which displays vertically stacked panes.
+The panes represent a hierarchy where a child pane contains the details of the
+current selection in its parent pane. The container provides simple primitives
+for panes to request changing their child pane:
+
+ +=<tr-ui-a-stacked-pane-view>=+ +=<tr-ui-a-stacked-pane-view>=+
+ |+.<tr-ui-a-stacked-pane>....+| |+.<tr-ui-a-stacked-pane>....+|
+ |: Pane 1 +| ===========> |: Pane 1 +|
+ |+...........................+| Pane 1 |+...........................+|
+ |+.<tr-ui-a-stacked-pane>....+| requests |+.<tr-ui-a-stacked-pane>....+|
+ |: Pane 2 (detail of Pane 1) +| child pane |: Pane 4 (detail of Pane 1) +|
+ |+...........................+| change (e.g. |+...........................+|
+ |+.<tr-ui-a-stacked-pane>....+| selection +=============================+
+ |: Pane 3 (detail of Pane 2) +| changed)
+ |+...........................+|
+ +=============================+
+
+Note that the actual UI provided by tr-ui-a-stacked-pane-view and
+tr-ui-a-stacked-pane is merely a wrapper container with flex box vertical
+stacking. No other visual features (such as pane spacing or borders) is
+provided by either element.
+
+The stacked pane element (tr-ui-a-stacked-pane) is defined in a separate file.
+
+Sample use case:
+
+ Create an empty stacked pane view and add it to the DOM:
+
+ const paneView = document.createElement('tr-ui-a-stacked-pane-view');
+ Polymer.dom(someParentView).appendChild(paneView);
+
+ Define one or more pane subclasses:
+
+ TODO(polymer): Write this documentation
+ <polymer-element name="some-pane-1" extends="tr-ui-a-stacked-pane">
+ ...
+ </polymer-element>
+
+ Set the top-level pane (by providing a builder function):
+
+ paneView.setPaneBuilder(function() {
+ const topPane = document.createElement('some-pane-1');
+ pane.someProperty = someValue;
+ return topPane;
+ });
+
+ Show a child pane with details upon user interaction (these methods should be
+ in the definition of the pane subclass Polymer element):
+
+ ready: function() {
+ this.$.table.addEventListener(
+ 'selection-changed', this.changeChildPane_.bind(this));
+ }
+
+ changeChildPane_: function() {
+ this.childPaneBuilder = function() {
+ const selectedRow = this.$.table.selectedTableRow;
+ const detailsPane = document.createElement('some-pane-2');
+ detailsPane.someProperty = selectedRow;
+ return detailsPane;
+ }.bind(this);
+ }
+-->
+<dom-module id='tr-ui-a-stacked-pane-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #pane_container > * {
+ flex: 0 0 auto;
+ }
+ </style>
+ <div id="pane_container">
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-stacked-pane-view',
+
+ /**
+ * Add a pane to the stacked pane view. This method performs two operations:
+ *
+ * 1. Remove existing descendant panes
+ * If the optional parent pane is provided, all its current descendant
+ * panes are removed. Otherwise, all panes are removed from the view.
+ *
+ * 2. Build and add new pane
+ * If a pane builder is provided and returns a pane, the new pane is
+ * appended to the view (after the provided parent, or at the top).
+ */
+ setPaneBuilder(paneBuilder, opt_parentPane) {
+ const paneContainer = this.$.pane_container;
+
+ // If the parent pane is provided, it must be an HTML element and a child
+ // of the pane container.
+ if (opt_parentPane) {
+ if (!(opt_parentPane instanceof HTMLElement)) {
+ throw new Error('Parent pane must be an HTML element');
+ }
+ if (opt_parentPane.parentElement !== paneContainer) {
+ throw new Error('Parent pane must be a child of the pane container');
+ }
+ }
+
+ // Remove all descendants of the parent pane (or all panes if no parent
+ // pane was specified) in reverse order.
+ while (Polymer.dom(paneContainer).lastElementChild !== null &&
+ Polymer.dom(paneContainer).lastElementChild !== opt_parentPane) {
+ const removedPane = Polymer.dom(this.$.pane_container).lastElementChild;
+ const listener = this.listeners_.get(removedPane);
+ if (listener === undefined) {
+ throw new Error('No listener associated with pane');
+ }
+ this.listeners_.delete(removedPane);
+ removedPane.removeEventListener(
+ 'request-child-pane-change', listener);
+ Polymer.dom(paneContainer).removeChild(removedPane);
+ }
+
+ if (opt_parentPane && opt_parentPane.parentElement !== paneContainer) {
+ throw new Error('Parent pane was removed from the pane container');
+ }
+
+ // This check is performed here (and not at the beginning of the method)
+ // because undefined pane builder means that the parent pane requested
+ // having no child pane (e.g. when selection is cleared).
+ if (!paneBuilder) return;
+
+ const pane = paneBuilder();
+ if (!pane) return;
+
+ if (!(pane instanceof HTMLElement)) {
+ throw new Error('Pane must be an HTML element');
+ }
+
+ // Listen for child pane change requests from the newly added pane.
+ const listener = function(event) {
+ this.setPaneBuilder(pane.childPaneBuilder, pane);
+ }.bind(this);
+ if (!this.listeners_) {
+ // Instead of initializing the listeners map in a created() callback,
+ // we do it lazily here so that subclasses could provide their own
+ // created() callback (Polymer currently doesn't allow calling overriden
+ // superclass methods in strict mode).
+ this.listeners_ = new WeakMap();
+ }
+ this.listeners_.set(pane, listener);
+ pane.addEventListener('request-child-pane-change', listener);
+
+ Polymer.dom(paneContainer).appendChild(pane);
+ pane.appended();
+ },
+
+ /**
+ * Request rebuilding all panes in the view. The panes are rebuilt from the
+ * top to the bottom (so that parent panes could request changing their
+ * child panes when they're being rebuilt and the newly constructed child
+ * panes would be rebuilt as well).
+ */
+ rebuild() {
+ let currentPane = Polymer.dom(this.$.pane_container).firstElementChild;
+ while (currentPane) {
+ currentPane.rebuild();
+ currentPane = currentPane.nextElementSibling;
+ }
+ },
+
+ // For testing purposes.
+ get panesForTesting() {
+ const panes = [];
+ let currentChild = Polymer.dom(this.$.pane_container).firstElementChild;
+ while (currentChild) {
+ panes.push(currentChild);
+ currentChild = currentChild.nextElementSibling;
+ }
+ return panes;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html
new file mode 100644
index 00000000000..ceae19ab0db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stacked_pane_view_test.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
+<link rel="import" href="/tracing/ui/analysis/stacked_pane_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createPaneView() {
+ return document.createElement('tr-ui-a-stacked-pane-view');
+ }
+
+ function createPane(paneId, opt_rebuildPaneCallback, opt_appendedCallback) {
+ const paneEl = document.createElement('tr-ui-a-stacked-pane');
+ paneEl.paneId = paneId;
+
+ const divEl = document.createElement('div');
+ Polymer.dom(divEl).textContent = 'Pane ' + paneId;
+ divEl.style.width = '400px';
+ divEl.style.background = '#ccc';
+ divEl.style.textAlign = 'center';
+ Polymer.dom(paneEl).appendChild(divEl);
+
+ if (opt_rebuildPaneCallback) {
+ paneEl.onRebuild_ = opt_rebuildPaneCallback;
+ }
+
+ if (opt_appendedCallback) {
+ paneEl.appended = opt_appendedCallback;
+ }
+
+ return paneEl;
+ }
+
+ function createPaneBuilder(paneId, opt_rebuildPaneCallback,
+ opt_appendedCallback) {
+ return createPane.bind(
+ undefined, paneId, opt_rebuildPaneCallback, opt_appendedCallback);
+ }
+
+ function assertPanes(paneView, expectedPaneIds) {
+ const actualPaneIds = paneView.panesForTesting.map(function(pane) {
+ return pane.paneId;
+ });
+ assert.deepEqual(actualPaneIds, expectedPaneIds);
+ }
+
+ test('instantiate_empty', function() {
+ const viewEl = createPaneView();
+ viewEl.rebuild();
+ assertPanes(viewEl, []);
+ // Don't add the pane to HTML output because it has zero height.
+ });
+
+ test('instantiate_singlePane', function() {
+ const viewEl = createPaneView();
+
+ viewEl.setPaneBuilder(createPaneBuilder(1));
+ viewEl.rebuild();
+
+ assertPanes(viewEl, [1]);
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('instantiate_multiplePanes', function() {
+ const viewEl = createPaneView();
+
+ viewEl.setPaneBuilder(createPaneBuilder(1));
+ viewEl.setPaneBuilder(createPaneBuilder(2), viewEl.panesForTesting[0]);
+ viewEl.setPaneBuilder(createPaneBuilder(3), viewEl.panesForTesting[1]);
+
+ assertPanes(viewEl, [1, 2, 3]);
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('changePanes', function() {
+ const viewEl = createPaneView();
+
+ viewEl.setPaneBuilder(createPaneBuilder(1));
+ assertPanes(viewEl, [1]);
+
+ viewEl.setPaneBuilder(null);
+ assertPanes(viewEl, []);
+
+ viewEl.setPaneBuilder(createPaneBuilder(2));
+ assertPanes(viewEl, [2]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(3), viewEl.panesForTesting[0]);
+ assertPanes(viewEl, [2, 3]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(4), viewEl.panesForTesting[0]);
+ assertPanes(viewEl, [2, 4]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(5), viewEl.panesForTesting[1]);
+ assertPanes(viewEl, [2, 4, 5]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(6), viewEl.panesForTesting[2]);
+ assertPanes(viewEl, [2, 4, 5, 6]);
+
+ viewEl.setPaneBuilder(createPaneBuilder(7), viewEl.panesForTesting[1]);
+ assertPanes(viewEl, [2, 4, 7]);
+
+ this.addHTMLOutput(viewEl);
+ });
+
+ test('childPanes', function() {
+ const viewEl = createPaneView();
+
+ viewEl.setPaneBuilder(createPaneBuilder(1));
+ assertPanes(viewEl, [1]);
+
+ // Pane 1 requests a child pane 2.
+ const pane1 = viewEl.panesForTesting[0];
+ pane1.childPaneBuilder = createPaneBuilder(2);
+ assertPanes(viewEl, [1, 2]);
+
+ // Pane 2 requests removing its child pane (nothing happens).
+ const pane2 = viewEl.panesForTesting[1];
+ pane2.childPaneBuilder = undefined;
+ assertPanes(viewEl, [1, 2]);
+
+ // Pane 2 requests a child pane 3.
+ pane2.childPaneBuilder = createPaneBuilder(3);
+ assertPanes(viewEl, [1, 2, 3]);
+
+ // Pane 2 requests a child pane 4 (its previous child pane 3 is removed).
+ pane2.childPaneBuilder = createPaneBuilder(4);
+ assertPanes(viewEl, [1, 2, 4]);
+
+ // Pane 1 requests removing its child pane (panes 2 and 4 are removed).
+ pane1.childPaneBuilder = undefined;
+ assertPanes(viewEl, [1]);
+
+ // Check that removed panes cannot affect the pane view.
+ pane2.childPaneBuilder = createPaneBuilder(5);
+ assertPanes(viewEl, [1]);
+
+ // Pane 1 requests a child pane 6 (check that everything still works).
+ pane1.childPaneBuilder = createPaneBuilder(6);
+ assertPanes(viewEl, [1, 6]);
+
+ // Change the top pane to pane 7.
+ viewEl.setPaneBuilder(createPaneBuilder(7));
+ assertPanes(viewEl, [7]);
+
+ // Check that removed panes cannot affect the pane view.
+ pane1.childPaneBuilder = createPaneBuilder(5);
+ assertPanes(viewEl, [7]);
+ });
+
+ test('rebuild', function() {
+ const viewEl = createPaneView();
+
+ const rebuiltPaneIds = [];
+ const rebuildPaneCallback = function() {
+ rebuiltPaneIds.push(this.paneId);
+ };
+
+ viewEl.setPaneBuilder(createPaneBuilder(1, rebuildPaneCallback));
+ viewEl.setPaneBuilder(createPaneBuilder(2, rebuildPaneCallback),
+ viewEl.panesForTesting[0]);
+ viewEl.setPaneBuilder(createPaneBuilder(3, rebuildPaneCallback),
+ viewEl.panesForTesting[1]);
+
+ // Rebuild isn't triggered.
+ assert.deepEqual(rebuiltPaneIds, []);
+
+ // Rebuild is triggered, but it isn't necessary (all panes are clean).
+ viewEl.rebuild();
+ assert.deepEqual(rebuiltPaneIds, []);
+
+ // All panes are now marked as dirty, but rebuild isn't triggered (it was
+ // only scheduled).
+ viewEl.panesForTesting.forEach(function(pane) {
+ pane.scheduleRebuild_();
+ });
+ assert.deepEqual(rebuiltPaneIds, []);
+
+ // Finally, rebuild was triggered and the panes are dirty.
+ viewEl.rebuild();
+ assert.deepEqual(rebuiltPaneIds, [1, 2, 3]);
+
+ // Make sure that panes are clean after the previous rebuild.
+ viewEl.rebuild();
+ assert.deepEqual(rebuiltPaneIds, [1, 2, 3]);
+ });
+
+ test('appended', function() {
+ const viewEl = createPaneView();
+ let didFireAppended;
+
+ didFireAppended = false;
+ viewEl.setPaneBuilder(createPaneBuilder(1, undefined, function() {
+ didFireAppended = true;
+ }));
+ assert.isTrue(didFireAppended);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html
new file mode 100644
index 00000000000..b371eac4cf9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/stub_analysis_table.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ function StubAnalysisTable() {
+ this.ownerDocument_ = document;
+ this.nodes_ = [];
+ }
+
+ StubAnalysisTable.prototype = {
+ __proto__: Object.protoype,
+
+ get ownerDocument() {
+ return this.ownerDocument_;
+ },
+
+ appendChild(node) {
+ if (node.tagName === 'TFOOT' || node.tagName === 'THEAD' ||
+ node.tagName === 'TBODY') {
+ node.__proto__ = StubAnalysisTable.prototype;
+ node.nodes_ = [];
+ node.ownerDocument_ = document;
+ }
+ this.nodes_.push(node);
+ },
+
+ get lastNode() {
+ return this.nodes_.pop();
+ },
+
+ get nodeCount() {
+ return this.nodes_.length;
+ }
+ };
+
+ return {
+ StubAnalysisTable,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html
new file mode 100644
index 00000000000..3c2bfdbd9cf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-a-user-expectation-related-samples-table'>
+ <template>
+ <style>
+ #table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-a-user-expectation-related-samples-table',
+
+ ready() {
+ this.samples_ = [];
+ this.$.table.tableColumns = [
+ {
+ title: 'Event(s)',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.type;
+ if (row.tooltip) {
+ typeEl.title = row.tooltip;
+ }
+ return typeEl;
+ },
+ width: '150px'
+ },
+ {
+ title: 'Link',
+ width: '100%',
+ value(row) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ if (row.name) {
+ linkEl.setSelectionAndContent(row.selection, row.name);
+ } else {
+ linkEl.selection = row.selection;
+ }
+ return linkEl;
+ }
+ }
+ ];
+ },
+
+ hasRelatedSamples() {
+ return (this.samples_ && this.samples_.length > 0);
+ },
+
+ set selection(eventSet) {
+ this.samples_ = [];
+ const samples = new tr.model.EventSet;
+ eventSet.forEach(function(ue) {
+ samples.addEventSet(ue.associatedSamples);
+ }.bind(this));
+
+ if (samples.length > 0) {
+ this.samples_.push({
+ type: 'Overlapping samples',
+ tooltip: 'All samples overlapping the selected user expectation(s).',
+ selection: samples
+ });
+ }
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ const table = this.$.table;
+ if (this.samples_ && this.samples_.length > 0) {
+ table.tableRows = this.samples_.slice();
+ } else {
+ table.tableRows = [];
+ }
+ table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html
new file mode 100644
index 00000000000..368f41ce6c6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/analysis/user_expectation_related_samples_table_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/sample.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import"
+ href="/tracing/ui/analysis/user_expectation_related_samples_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+ const node = tr.c.TestUtils.newProfileNodes(m, ['fake']);
+ const s1 = new tr.model.Sample(1, 'a_1', node, m.t2);
+ const s2 = new tr.model.Sample(2, 'a_2', node, m.t2);
+ const s3 = new tr.model.Sample(3, 'a_3', node, m.t2);
+ const s4 = new tr.model.Sample(4, 'a_4', node, m.t2);
+ const s5 = new tr.model.Sample(5, 'a_5', node, m.t2);
+ const s6 = new tr.model.Sample(6, 'a_6', node, m.t2);
+ m.samples.push(s1, s2, s3, s4, s5, s6);
+ m.ve = new tr.c.TestUtils.newSliceEx(
+ {title: 'V8.Execute', start: 0, end: 4, type: tr.model.ThreadSlice});
+ m.t2.sliceGroup.pushSlice(m.ve);
+ m.up = new tr.c.TestUtils.newInteractionRecord(m, 0, 4);
+ m.up.associatedEvents.push(m.ve);
+ m.userModel.expectations.push(m.up);
+ });
+ return m;
+ }
+
+ test('overlappingSamples', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement(
+ 'tr-ui-a-user-expectation-related-samples-table');
+ viewEl.selection = new tr.model.EventSet([m.up]);
+
+ let overlappingSamples;
+ viewEl.$.table.tableRows.forEach(function(row) {
+ if (row.type === 'Overlapping samples') {
+ assert.isUndefined(overlappingSamples);
+ overlappingSamples = row.selection;
+ }
+ });
+
+ const samplesTitles = overlappingSamples.map(function(e) {
+ return e.title;
+ });
+ assert.sameMembers(samplesTitles,
+ ['a_1', 'a_2', 'a_3', 'a_4']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html
new file mode 100644
index 00000000000..48ba2ac9fa7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.annotations', function() {
+ /**
+ * A base class for all annotation views.
+ * @constructor
+ */
+ function AnnotationView(viewport, annotation) {
+ }
+
+ AnnotationView.prototype = {
+ draw(ctx) {
+ throw new Error('Not implemented');
+ }
+ };
+
+ return {
+ AnnotationView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html
new file mode 100644
index 00000000000..3a3b3615f91
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/annotation_view_test.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/comment_box_annotation.html">
+<link rel="import" href="/tracing/model/location.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/rect_annotation.html">
+<link rel="import" href="/tracing/model/x_marker_annotation.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createPopulatedTimeline() {
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(2);
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'a', start: 80, duration: 50}));
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ const vp = new tr.ui.TimelineViewport(timeline);
+ timeline.model = model;
+ timeline.style.maxHeight = '600px';
+
+ return timeline;
+ }
+
+ test('rectAnnotation', function() {
+ const fakeYComponents1 = [{stableId: '1.2', yPercentOffset: 0.3}];
+ const fakeYComponents2 = [{stableId: '1.2', yPercentOffset: 0.9}];
+ const start = new tr.model.Location(50, fakeYComponents1);
+ const end = new tr.model.Location(100, fakeYComponents2);
+ const rectAnnotation = new tr.model.RectAnnotation(start, end);
+
+ const timeline = createPopulatedTimeline();
+ timeline.model.addAnnotation(rectAnnotation);
+ this.addHTMLOutput(timeline);
+ });
+
+ test('xMarkerAnnotation', function() {
+ const xMarkerAnnotation = new tr.model.XMarkerAnnotation(90);
+
+ const timeline = createPopulatedTimeline();
+ const model = timeline.model;
+ timeline.model.addAnnotation(xMarkerAnnotation);
+ this.addHTMLOutput(timeline);
+ });
+
+ test('commentBoxAnnotation', function() {
+ const fakeYComponents = [{stableId: '1.2', yPercentOffset: 0.5}];
+ const location = new tr.model.Location(120, fakeYComponents);
+ const text = 'abc';
+ const commentBoxAnnotation =
+ new tr.model.CommentBoxAnnotation(location, text);
+
+ const timeline = createPopulatedTimeline();
+ timeline.model.addAnnotation(commentBoxAnnotation);
+ this.addHTMLOutput(timeline);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html
new file mode 100644
index 00000000000..5237b9b0f55
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/comment_box_annotation_view.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/annotations/annotation_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.annotations', function() {
+ /**
+ * A view of a comment box consisting of a textarea and a line to the
+ * actual location.
+ * @extends {AnnotationView}
+ * @constructor
+ */
+ function CommentBoxAnnotationView(viewport, annotation) {
+ this.viewport_ = viewport;
+ this.annotation_ = annotation;
+ this.textArea_ = undefined;
+
+ this.styleWidth = 250;
+ this.styleHeight = 50;
+ this.fontSize = 10;
+ this.rightOffset = 50;
+ this.topOffset = 25;
+ }
+
+ CommentBoxAnnotationView.prototype = {
+ __proto__: tr.ui.annotations.AnnotationView.prototype,
+
+ removeTextArea() {
+ Polymer.dom(Polymer.dom(this.textArea_).parentNode).removeChild(
+ this.textArea_);
+ },
+
+ draw(ctx) {
+ const coords = this.annotation_.location.toViewCoordinates(
+ this.viewport_);
+ if (coords.viewX < 0) {
+ if (this.textArea_) {
+ this.textArea_.style.visibility = 'hidden';
+ }
+ return;
+ }
+
+ // Set up textarea element.
+ if (!this.textArea_) {
+ this.textArea_ = document.createElement('textarea');
+ this.textArea_.style.position = 'absolute';
+ this.textArea_.readOnly = true;
+ this.textArea_.value = this.annotation_.text;
+ // Set the z-index so that this is shown on top of canvas.
+ this.textArea_.style.zIndex = 1;
+ Polymer.dom(Polymer.dom(ctx.canvas).parentNode)
+ .appendChild(this.textArea_);
+ }
+
+ this.textArea_.style.width = this.styleWidth + 'px';
+ this.textArea_.style.height = this.styleHeight + 'px';
+ this.textArea_.style.fontSize = this.fontSize + 'px';
+ this.textArea_.style.visibility = 'visible';
+
+ // Update positions to latest coordinate.
+ this.textArea_.style.left =
+ coords.viewX + ctx.canvas.getBoundingClientRect().left +
+ this.rightOffset + 'px';
+ this.textArea_.style.top =
+ coords.viewY - ctx.canvas.getBoundingClientRect().top -
+ this.topOffset + 'px';
+
+ // Draw pointer line from offset to actual location.
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ tr.ui.b.drawLine(ctx, coords.viewX,
+ coords.viewY - ctx.canvas.getBoundingClientRect().top,
+ coords.viewX + this.rightOffset,
+ coords.viewY - this.topOffset -
+ ctx.canvas.getBoundingClientRect().top);
+ ctx.stroke();
+ }
+ };
+
+ return {
+ CommentBoxAnnotationView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html
new file mode 100644
index 00000000000..16e5f920eea
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/rect_annotation_view.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/annotations/annotation_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.annotations', function() {
+ /**
+ * A view responsible for drawing a single highlight rectangle box on
+ * the timeline.
+ * @extends {AnnotationView}
+ * @constructor
+ */
+ function RectAnnotationView(viewport, annotation) {
+ this.viewport_ = viewport;
+ this.annotation_ = annotation;
+ }
+
+ RectAnnotationView.prototype = {
+ __proto__: tr.ui.annotations.AnnotationView.prototype,
+
+ draw(ctx) {
+ const dt = this.viewport_.currentDisplayTransform;
+ const startCoords =
+ this.annotation_.startLocation.toViewCoordinates(this.viewport_);
+ const endCoords =
+ this.annotation_.endLocation.toViewCoordinates(this.viewport_);
+
+ // Prevent drawing into the ruler track by clamping the initial Y
+ // point and the rect's Y size.
+ let startY = startCoords.viewY - ctx.canvas.getBoundingClientRect().top;
+ const sizeY = endCoords.viewY - startCoords.viewY;
+ if (startY + sizeY < 0) {
+ // In this case sizeY is negative. If final Y is negative,
+ // overwrite startY so that the rectangle ends at y=0.
+ startY = sizeY;
+ } else if (startY < 0) {
+ startY = 0;
+ }
+
+ ctx.fillStyle = this.annotation_.fillStyle;
+ ctx.fillRect(startCoords.viewX, startY,
+ endCoords.viewX - startCoords.viewX, sizeY);
+ }
+ };
+
+ return {
+ RectAnnotationView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html b/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html
new file mode 100644
index 00000000000..933ea17ca57
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/annotations/x_marker_annotation_view.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/annotations/annotation_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.annotations', function() {
+ /**
+ * A view that draws a vertical line on the timeline at a specific timestamp.
+ * @extends {AnnotationView}
+ * @constructor
+ */
+ function XMarkerAnnotationView(viewport, annotation) {
+ this.viewport_ = viewport;
+ this.annotation_ = annotation;
+ }
+
+ XMarkerAnnotationView.prototype = {
+ __proto__: tr.ui.annotations.AnnotationView.prototype,
+
+ draw(ctx) {
+ const dt = this.viewport_.currentDisplayTransform;
+ const viewX = dt.xWorldToView(this.annotation_.timestamp);
+
+ ctx.beginPath();
+ tr.ui.b.drawLine(ctx, viewX, 0, viewX, ctx.canvas.height);
+ ctx.strokeStyle = this.annotation_.strokeStyle;
+ ctx.stroke();
+ }
+ };
+
+ return {
+ XMarkerAnnotationView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html
new file mode 100644
index 00000000000..46c62818fc3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Represents a procedural animation that can be run by an
+ * tr.ui.b.AnimationController.
+ *
+ * @constructor
+ */
+ function Animation() {
+ }
+
+ Animation.prototype = {
+
+ /**
+ * Called when an animation has been queued after a running animation.
+ *
+ * @return {boolean} True if the animation can take on the responsibilities
+ * of the running animation. If true, takeOverFor will be called on the
+ * animation.
+ *
+ * This can be used to build animations that accelerate as pairs of them are
+ * queued.
+ */
+ canTakeOverFor(existingAnimation) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Called to take over responsiblities of an existingAnimation.
+ *
+ * At this point, the existingAnimation has been ticked one last time, then
+ * stopped. This animation will be started after this returns and has the
+ * job of finishing(or transitioning away from) the effect the existing
+ * animation was trying to accomplish.
+ */
+ takeOverFor(existingAnimation, newStartTimestamp, target) {
+ throw new Error('Not implemented');
+ },
+
+ start(timestamp, target) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Called when an animation is stopped before it finishes. The animation can
+ * do what it wants here, usually nothing.
+ *
+ * @param {Number} timestamp When the animation was stopped.
+ * @param {Object} target The object being animated. May be undefined, take
+ * care.
+ * @param {boolean} willBeTakenOverByAnotherAnimation Whether this animation
+ * is going to be handed to another animation's takeOverFor function.
+ */
+ didStopEarly(timestamp, target,
+ willBeTakenOverByAnotherAnimation) {
+ },
+
+ /**
+ * @return {boolean} true if the animation is finished.
+ */
+ tick(timestamp, target) {
+ throw new Error('Not implemented');
+ }
+ };
+
+ return {
+ Animation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html
new file mode 100644
index 00000000000..a6149cba5fb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/animation.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Manages execution, queueing and blending of tr.ui.b.Animations against
+ * a single target.
+ *
+ * Targets must have a cloneAnimationState() method that returns all the
+ * animatable states of that target.
+ *
+ * @constructor
+ * @extends {tr.b.EventTarget}
+ */
+ function AnimationController() {
+ tr.b.EventTarget.call(this);
+
+ this.target_ = undefined;
+
+ this.activeAnimation_ = undefined;
+
+ this.tickScheduled_ = false;
+ }
+
+ AnimationController.prototype = {
+ __proto__: tr.b.EventTarget.prototype,
+
+ get target() {
+ return this.target_;
+ },
+
+ set target(target) {
+ if (this.activeAnimation_) {
+ throw new Error('Cannot change target while animation is running.');
+ }
+ if (target.cloneAnimationState === undefined ||
+ typeof target.cloneAnimationState !== 'function') {
+ throw new Error('target must have a cloneAnimationState function');
+ }
+
+ this.target_ = target;
+ },
+
+ get activeAnimation() {
+ return this.activeAnimation_;
+ },
+
+ get hasActiveAnimation() {
+ return !!this.activeAnimation_;
+ },
+
+ queueAnimation(animation, opt_now) {
+ if (this.target_ === undefined) {
+ throw new Error('Cannot queue animations without a target');
+ }
+
+ let now;
+ if (opt_now !== undefined) {
+ now = opt_now;
+ } else {
+ now = window.performance.now();
+ }
+
+ if (this.activeAnimation_) {
+ // Must tick the animation before stopping it case its about to stop,
+ // and to update the target with its final sets of edits up to this
+ // point.
+ const done = this.activeAnimation_.tick(now, this.target_);
+ if (done) {
+ this.activeAnimation_ = undefined;
+ }
+ }
+
+ if (this.activeAnimation_) {
+ if (animation.canTakeOverFor(this.activeAnimation_)) {
+ this.activeAnimation_.didStopEarly(now, this.target_, true);
+ animation.takeOverFor(this.activeAnimation_, now, this.target_);
+ } else {
+ this.activeAnimation_.didStopEarly(now, this.target_, false);
+ }
+ }
+ this.activeAnimation_ = animation;
+ this.activeAnimation_.start(now, this.target_);
+
+ if (this.tickScheduled_) return;
+ this.tickScheduled_ = true;
+ tr.b.requestAnimationFrame(this.tickActiveAnimation_, this);
+ },
+
+ cancelActiveAnimation(opt_now) {
+ if (!this.activeAnimation_) return;
+ let now;
+ if (opt_now !== undefined) {
+ now = opt_now;
+ } else {
+ now = window.performance.now();
+ }
+ this.activeAnimation_.didStopEarly(now, this.target_, false);
+ this.activeAnimation_ = undefined;
+ },
+
+ tickActiveAnimation_(frameBeginTime) {
+ this.tickScheduled_ = false;
+ if (!this.activeAnimation_) return;
+
+ if (this.target_ === undefined) {
+ this.activeAnimation_.didStopEarly(frameBeginTime, this.target_, false);
+ return;
+ }
+
+ const oldTargetState = this.target_.cloneAnimationState();
+
+ const done = this.activeAnimation_.tick(frameBeginTime, this.target_);
+ if (done) {
+ this.activeAnimation_ = undefined;
+ }
+
+ if (this.activeAnimation_) {
+ this.tickScheduled_ = true;
+ tr.b.requestAnimationFrame(this.tickActiveAnimation_, this);
+ }
+
+ if (oldTargetState) {
+ const e = new tr.b.Event('didtick');
+ e.oldTargetState = oldTargetState;
+ this.dispatchEvent(e, false, false);
+ }
+ }
+ };
+
+ return {
+ AnimationController,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html
new file mode 100644
index 00000000000..9366ab4db2b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/animation_controller_test.html
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/animation_controller.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function SimpleAnimation(options) {
+ this.stopTime = options.stopTime;
+
+ this.startCalled = false;
+ this.didStopEarlyCalled = false;
+ this.wasTakenOver = false;
+ this.tickCount = 0;
+ }
+
+ SimpleAnimation.prototype = {
+ __proto__: tr.ui.b.Animation.prototype,
+
+ canTakeOverFor(existingAnimation) {
+ return false;
+ },
+
+ takeOverFor(existingAnimation, newStartTimestamp, target) {
+ throw new Error('Not implemented');
+ },
+
+ start(timestamp, target) {
+ this.startCalled = true;
+ },
+
+ didStopEarly(timestamp, target, willBeTakenOver) {
+ this.didStopEarlyCalled = true;
+ this.wasTakenOver = willBeTakenOver;
+ },
+
+ /**
+ * @return {boolean} true if the animation is finished.
+ */
+ tick(timestamp, target) {
+ this.tickCount++;
+ return timestamp >= this.stopTime;
+ }
+ };
+
+ test('cancel', function() {
+ const target = {
+ x: 0,
+ cloneAnimationState() { return {x: this.x}; }
+ };
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+
+ const animation = new SimpleAnimation({stopTime: 100});
+ controller.queueAnimation(animation);
+
+ tr.b.forcePendingRAFTasksToRun(0);
+ assert.strictEqual(animation.tickCount, 1);
+ controller.cancelActiveAnimation();
+ assert.isFalse(controller.hasActiveAnimation);
+ assert.isTrue(animation.didStopEarlyCalled);
+ });
+
+ test('simple', function() {
+ const target = {
+ x: 0,
+ cloneAnimationState() { return {x: this.x}; }
+ };
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+
+ const animation = new SimpleAnimation({stopTime: 100});
+ controller.queueAnimation(animation);
+
+ tr.b.forcePendingRAFTasksToRun(0);
+ assert.strictEqual(animation.tickCount, 1);
+ assert.isTrue(controller.hasActiveAnimation);
+
+ tr.b.forcePendingRAFTasksToRun(100);
+ assert.strictEqual(animation.tickCount, 2);
+ assert.isFalse(controller.hasActiveAnimation);
+ });
+
+ test('queueTwo', function() {
+ // Clear all pending rafs so if something is lingering it will blow up here.
+ tr.b.forcePendingRAFTasksToRun(0);
+
+ const target = {
+ x: 0,
+ cloneAnimationState() { return {x: this.x}; }
+ };
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+
+ const a1 = new SimpleAnimation({stopTime: 100});
+ const a2 = new SimpleAnimation({stopTime: 100});
+ controller.queueAnimation(a1, 0);
+ assert.isTrue(a1.startCalled);
+ controller.queueAnimation(a2, 50);
+ assert.isTrue(a1.didStopEarlyCalled);
+ assert.isTrue(a2.startCalled);
+
+ tr.b.forcePendingRAFTasksToRun(150);
+ assert.isFalse(controller.hasActiveAnimation);
+ assert.isAbove(a2.tickCount, 0);
+ });
+
+ /**
+ * @constructor
+ */
+ function AnimationThatCanTakeOverForSimpleAnimation() {
+ this.takeOverForAnimation = undefined;
+ }
+
+ AnimationThatCanTakeOverForSimpleAnimation.prototype = {
+ __proto__: tr.ui.b.Animation.prototype,
+
+
+ canTakeOverFor(existingAnimation) {
+ return existingAnimation instanceof SimpleAnimation;
+ },
+
+ takeOverFor(existingAnimation, newStartTimestamp, target) {
+ this.takeOverForAnimation = existingAnimation;
+ },
+
+ start(timestamp, target) {
+ this.startCalled = true;
+ }
+ };
+
+ test('takeOver', function() {
+ const target = {
+ x: 0,
+ cloneAnimationState() { return {x: this.x}; }
+ };
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+
+ const a1 = new SimpleAnimation({stopTime: 100});
+ const a2 = new AnimationThatCanTakeOverForSimpleAnimation();
+ controller.queueAnimation(a1, 0);
+ assert.isTrue(a1.startCalled);
+ assert.strictEqual(a1.tickCount, 0);
+ controller.queueAnimation(a2, 10);
+ assert.isTrue(a1.didStopEarlyCalled);
+ assert.isTrue(a1.wasTakenOver);
+ assert.strictEqual(a1.tickCount, 1);
+
+ assert.strictEqual(a1, a2.takeOverForAnimation);
+ assert.isTrue(a2.startCalled);
+
+ controller.cancelActiveAnimation();
+ assert.isFalse(controller.hasActiveAnimation);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html
new file mode 100644
index 00000000000..1745861cbe4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart.html
@@ -0,0 +1,253 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const BarChart = tr.ui.b.define('bar-chart', tr.ui.b.ColumnChart);
+
+ BarChart.prototype = {
+ __proto__: tr.ui.b.ColumnChart.prototype,
+
+ decorate() {
+ super.decorate();
+ this.verticalScale_ = undefined;
+ this.horizontalScale_ = undefined;
+ this.isWaterfall_ = false;
+ },
+
+ updateScales_() {
+ super.updateScales_();
+ this.yScale_.range([this.graphWidth, 0]);
+ this.xScale_.range([0, this.graphHeight]);
+ this.verticalScale_ = this.isYLogScale_ ? d3.scale.log(10) :
+ d3.scale.linear();
+ this.verticalScale_.domain(this.xScale_.domain());
+ this.verticalScale_.range([this.graphHeight, 0]);
+ this.horizontalScale_ = d3.scale.linear();
+ this.horizontalScale_.domain(this.yScale_.domain());
+ this.horizontalScale_.range([0, this.graphWidth]);
+ },
+
+ set isWaterfall(waterfall) {
+ this.isWaterfall_ = waterfall;
+ if (waterfall) {
+ this.getDataSeries('hide').color = 'transparent';
+ }
+ this.updateContents_();
+ },
+
+ get isWaterfall() {
+ return this.isWaterfall_;
+ },
+
+ get defaultGraphHeight() {
+ return Math.max(20, 10 * this.data_.length);
+ },
+
+ get defaultGraphWidth() {
+ return 100;
+ },
+
+ get barHeight() {
+ return this.graphHeight / this.data.length;
+ },
+
+ drawBrush_(brushRectsSel) {
+ brushRectsSel
+ .attr('x', 0)
+ .attr('width', this.graphWidth)
+ .attr('y', d => this.verticalScale_(d.max))
+ .attr('height', d =>
+ this.verticalScale_(d.min) - this.verticalScale_(d.max))
+ .attr('fill', 'rgb(213, 236, 229)');
+ },
+
+ getDataPointAtChartPoint_(chartPoint) {
+ const flippedPoint = {
+ x: this.graphHeight - chartPoint.y,
+ y: this.graphWidth - chartPoint.x
+ };
+ return super.getDataPointAtChartPoint_(flippedPoint);
+ },
+
+ drawXAxis_(xAxis) {
+ xAxis.attr('transform', 'translate(0,' + this.graphHeight + ')')
+ .call(d3.svg.axis()
+ .scale(this.horizontalScale_)
+ .orient('bottom'));
+ },
+
+ get yAxisWidth() {
+ return this.computeScaleTickWidth_(this.verticalScale_);
+ },
+
+ drawYAxis_(yAxis) {
+ const axisModifier = d3.svg.axis()
+ .scale(this.verticalScale_)
+ .orient('left');
+ yAxis.call(axisModifier);
+ },
+
+ drawHoverValueBox_(rect) {
+ const rectHoverEvent = new tr.b.Event('rect-mouseenter');
+ rectHoverEvent.rect = rect;
+ this.dispatchEvent(rectHoverEvent);
+
+ if (!this.enableHoverBox || (this.isWaterfall_ && rect.key === 'hide')) {
+ return;
+ }
+
+ const seriesKeys = [...this.seriesByKey_.keys()];
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ chartAreaSel.selectAll('.hover').remove();
+ let keyWidthPx = 0;
+ let keyHeightPx = 0;
+ let xWidthPx = 0;
+ let xHeightPx = 0;
+ let groupWidthPx = 0;
+ let groupHeightPx = 0;
+ if (seriesKeys.length > 1 && !this.isGrouped && !this.isWaterfall_) {
+ keyWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.key).width;
+ keyHeightPx = this.textHeightPx_;
+ }
+ if (this.data.length > 1 && !this.isWaterfall_) {
+ xWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, '' + rect.datum.x).width;
+ xHeightPx = this.textHeightPx_;
+ }
+ if (this.isGrouped && rect.datum.group !== undefined) {
+ groupWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.datum.group).width;
+ groupHeightPx = this.textHeightPx_;
+ }
+ const valueWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.value).width;
+ const valueHeightPx = this.textHeightPx_;
+ const maxWidthPx = Math.max(keyWidthPx, xWidthPx,
+ groupWidthPx, valueWidthPx) + 5;
+ const hoverWidthPx = this.isGrouped ? maxWidthPx : Math.min(maxWidthPx,
+ Math.max(50, rect.widthPx));
+ let hoverTopPx = rect.topPx;
+ hoverTopPx = Math.min(
+ hoverTopPx, this.getBoundingClientRect().height -
+ valueHeightPx);
+ let hoverLeftPx = rect.leftPx + (rect.widthPx / 2);
+ hoverLeftPx = Math.max(hoverLeftPx - hoverWidthPx, -this.margin.left);
+
+ chartAreaSel
+ .append('rect')
+ .attr('class', 'hover')
+ .attr('fill', 'white')
+ .attr('x', hoverLeftPx)
+ .attr('y', hoverTopPx)
+ .attr('width', hoverWidthPx)
+ .attr('height', keyHeightPx + xHeightPx +
+ valueHeightPx + groupHeightPx);
+
+ if (seriesKeys.length > 1 && !this.isGrouped && !this.isWaterfall_) {
+ chartAreaSel
+ .append('text')
+ .attr('class', 'hover')
+ .attr('fill', rect.color === 'transparent' ? '#000000' : rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx - 3)
+ .text(rect.key);
+ }
+ if (this.data.length > 1 && !this.isWaterfall_) {
+ chartAreaSel
+ .append('text')
+ .attr('class', 'hover')
+ .attr('fill', rect.color === 'transparent' ? '#000000' : rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx + valueHeightPx - 3)
+ .text('' + rect.datum.x);
+ }
+ if (this.isGrouped && rect.datum.group !== undefined) {
+ chartAreaSel
+ .append('text')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .attr('class', 'hover')
+ .attr('fill', rect.color === 'transparent' ? '#000000' : rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx + xHeightPx + groupHeightPx - 3)
+ .text(rect.datum.group);
+ }
+ chartAreaSel
+ .append('text')
+ .attr('class', 'hover')
+ .attr('fill', rect.color === 'transparent' ? '#000000' : rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + xHeightPx + keyHeightPx +
+ groupHeightPx + valueHeightPx - 3)
+ .text(rect.value);
+ },
+
+ flipRect_(rect) {
+ // Flip |rect| around |y=x|.
+ return {
+ datum: rect.datum,
+ index: rect.index,
+ key: rect.key,
+ value: rect.value,
+ color: rect.color,
+ topPx: this.graphHeight - rect.leftPx - rect.widthPx,
+ leftPx: this.graphWidth - rect.topPx - rect.heightPx,
+ widthPx: rect.heightPx,
+ heightPx: rect.widthPx,
+ underflow: rect.underflow,
+ overflow: rect.overflow,
+ };
+ },
+
+ drawRect_(rect, sel) {
+ super.drawRect_(this.flipRect_(rect), sel);
+ },
+
+ drawUnderflow_(rect, rectsSel) {
+ let sel = rectsSel.data([rect]);
+ sel.enter().append('text')
+ .text('*')
+ .attr('fill', rect.color)
+ .attr('x', 0)
+ .attr('y', this.graphHeight - rect.leftPx +
+ 3 + (rect.widthPx / 2));
+ sel.exit().remove();
+
+ sel = rectsSel.data([rect]);
+ sel.enter().append('rect')
+ .attr('fill', 'rgba(0, 0, 0, 0)')
+ .attr('x', 0)
+ .attr('y', this.graphHeight - rect.leftPx - rect.widthPx)
+ .attr('width', 10)
+ .attr('height', rect.widthPx)
+ .on('mouseenter', () => this.drawHoverValueBox_(this.flipRect_(rect)))
+ .on('mouseleave', () => this.clearHoverValueBox_(rect));
+ sel.exit().remove();
+ },
+
+ drawOverflow_(rect, sel) {
+ sel = sel.data([rect]);
+ sel.enter().append('text')
+ .text('*')
+ .attr('fill', rect.color)
+ .attr('x', this.graphWidth)
+ .attr('y', this.graphHeight - rect.leftPx +
+ 3 + (rect.widthPx / 2));
+ sel.exit().remove();
+ }
+ };
+
+ return {
+ BarChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html
new file mode 100644
index 00000000000..48e5ff778aa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/bar_chart_test.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/assert_utils.html">
+<link rel="import" href="/tracing/ui/base/bar_chart.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 100},
+ {x: 20, value: 110},
+ {x: 30, value: 100},
+ {x: 40, value: 50}
+ ];
+ });
+
+ test('instantiation_singleDatum', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 100},
+ ];
+ });
+
+ test('instantiation_stacked', function() {
+ const chart = new tr.ui.b.BarChart();
+ chart.isStacked = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, foo: 10, bar: 5, qux: 7},
+ {x: 20, foo: 11, bar: 6, qux: 3},
+ {x: 30, foo: 10, bar: 4, qux: 8},
+ {x: 40, foo: 5, bar: 1, qux: 2}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.BarChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50},
+ {x: 20, alpha: 110, beta: 75},
+ {x: 30, alpha: 100, beta: 125},
+ {x: 40, alpha: 50, beta: 125}
+ ];
+ chart.brushedRange = tr.b.math.Range.fromExplicitRange(20, 40);
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: undefined},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 25, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: 40},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.BarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 50},
+ {x: 20, value: 60},
+ {x: 30, value: 80},
+ {x: 40, value: 20},
+ {x: 50, value: 30},
+ {x: 60, value: 20},
+ {x: 70, value: 15},
+ {x: 80, value: 20}
+ ];
+
+ let mouseDownX = undefined;
+ let curMouseX = undefined;
+
+ function updateBrushedRange() {
+ if (mouseDownX === undefined || (mouseDownX === curMouseX)) {
+ chart.brushedRange = new tr.b.math.Range();
+ return;
+ }
+ const r = new tr.b.math.Range();
+ r.min = Math.min(mouseDownX, curMouseX);
+ r.max = Math.max(mouseDownX, curMouseX);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownX = e.x;
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ });
+
+ test('instantiation_overrideDataRange', function() {
+ let chart = new tr.ui.b.BarChart();
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90);
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: -20},
+ {x: 1, value: 100},
+ {x: 2, value: -40},
+ {x: 3, value: 100},
+ ];
+
+ chart = new tr.ui.b.BarChart();
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100);
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 50},
+ ];
+ });
+
+ test('instantiation_Waterfall', function() {
+ const chart = new tr.ui.b.BarChart();
+ chart.graphWidth = 300;
+ chart.graphHeight = 200;
+ chart.isStacked = true;
+ chart.isGrouped = true;
+ chart.isWaterfall = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, alpha: 40, group: 'group1' },
+ {x: 1, alpha: 30, group: 'group2' },
+ {x: 2},
+ {x: 3, hide: 40, beta: 55, group: 'group1' },
+ {x: 4, hide: 40, beta: 65, group: 'group2' },
+ {x: 5},
+ {x: 6, hide: 95, omega: 10, group: 'group1' },
+ {x: 7, hide: 95, omega: 20, group: 'group2' }
+ ];
+ });
+
+ test('instantiation_showHoverValuesForTransparentData', function() {
+ const chart = new tr.ui.b.BarChart();
+ chart.graphWidth = 300;
+ chart.graphHeight = 200;
+ chart.isStacked = true;
+ chart.isGrouped = true;
+ chart.displayXInHover = true;
+ chart.getDataSeries('alpha').color = 'transparent';
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, alpha: 40, beta: 32, omega: 13, group: 'group1' },
+ {x: 1, alpha: 30, beta: 22, omega: 14, group: 'group2' },
+ {x: 2},
+ {x: 3, alpha: 55, beta: 35, omega: 15, group: 'group1' },
+ {x: 4, alpha: 45, beta: 40, omega: 16, group: 'group2' },
+ {x: 5},
+ {x: 6, alpha: 50, beta: 10, omega: 17, group: 'group1' },
+ {x: 7, alpha: 60, beta: 15, omega: 18, group: 'group2' }
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/base.html b/chromium/third_party/catapult/tracing/tracing/ui/base/base.html
new file mode 100644
index 00000000000..e0ca9e23c6b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/base.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/polymer_preload.html" data-suppress-import-order>
+
+<!--
+Polymer is imported through third-party HTML files, which means that we have to
+manually list all recursive imports.
+-->
+<link rel="import" href="/components/polymer/polymer-micro.html" data-suppress-import-order>
+<link rel="import" href="/components/polymer/polymer-mini.html" data-suppress-import-order>
+<link rel="import" href="/components/polymer/polymer.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/ui/base/polymer_postload.html" data-suppress-import-order>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html
new file mode 100644
index 00000000000..9138d1ea8b0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/name_column_chart.html">
+<link rel="import" href="/tracing/ui/base/name_line_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const BoxChart = tr.ui.b.define('box-chart', tr.ui.b.NameLineChart);
+
+ BoxChart.prototype = {
+ __proto__: tr.ui.b.NameLineChart.prototype,
+
+ get hideLegend() {
+ return true;
+ },
+
+ updateDataRange_() {
+ if (this.overrideDataRange_ !== undefined) {
+ return;
+ }
+
+ this.autoDataRange_.reset();
+ for (const datum of this.data_) {
+ this.autoDataRange_.addValue(datum.percentile_0);
+ this.autoDataRange_.addValue(datum.percentile_100);
+ }
+ },
+
+ updateScales_() {
+ super.updateScales_();
+ this.xScale_.domain([0, this.data_.length]);
+ },
+
+ get xAxisTickOffset() {
+ return 0.5;
+ },
+
+ updateDataRange_() {
+ if (this.overrideDataRange_ !== undefined) return;
+
+ this.autoDataRange_.reset();
+ for (const datum of this.data_) {
+ this.autoDataRange_.addValue(datum.percentile_0);
+ this.autoDataRange_.addValue(datum.percentile_100);
+ }
+ },
+
+ updateXAxis_(xAxis) {
+ xAxis.selectAll('*').remove();
+ if (this.hideXAxis) return;
+
+ tr.ui.b.NameColumnChart.prototype.updateXAxis_.call(this, xAxis);
+
+ const baseline = xAxis.selectAll('path').data([this]);
+ baseline.enter().append('line')
+ .attr('stroke', 'black')
+ .attr('x1', this.xScale_(0))
+ .attr('x2', this.xScale_(this.data_.length))
+ .attr('y1', this.graphHeight)
+ .attr('y2', this.graphHeight);
+ baseline.exit().remove();
+ },
+
+ updateDataContents_(dataSel) {
+ dataSel.selectAll('*').remove();
+ const boxesSel = dataSel.selectAll('path');
+ for (let index = 0; index < this.data_.length; ++index) {
+ const datum = this.data_[index];
+ const color = datum.color || 'black';
+
+ // Draw a box between percentiles 25 and 75:
+ let sel = boxesSel.data([datum]);
+ sel.enter().append('rect')
+ .attr('fill', color)
+ .attr('x', this.xScale_(index + 0.2))
+ .attr('width',
+ this.xScale_(index + 0.8) - this.xScale_(index + 0.2))
+ .attr('y', this.yScale_(datum.percentile_75))
+ .attr('height', this.yScale_(datum.percentile_25) -
+ this.yScale_(datum.percentile_75));
+ sel.exit().remove();
+
+ // Draw a horizontal line for percentile_50:
+ sel = boxesSel.data([datum]);
+ sel.enter().append('line')
+ .attr('stroke', color)
+ .attr('x1', this.xScale_(index))
+ .attr('x2', this.xScale_(index + 1))
+ .attr('y1', this.yScale_(datum.percentile_50))
+ .attr('y2', this.yScale_(datum.percentile_50));
+ sel.exit().remove();
+
+ // Draw two shorter horizontal lines for percentiles 0 and 100:
+ sel = boxesSel.data([datum]);
+ sel.enter().append('line')
+ .attr('stroke', color)
+ .attr('x1', this.xScale_(index + 0.4))
+ .attr('x2', this.xScale_(index + 0.6))
+ .attr('y1', this.yScale_(datum.percentile_0))
+ .attr('y2', this.yScale_(datum.percentile_0));
+ sel.exit().remove();
+ sel = boxesSel.data([datum]);
+ sel.enter().append('line')
+ .attr('stroke', color)
+ .attr('x1', this.xScale_(index + 0.4))
+ .attr('x2', this.xScale_(index + 0.6))
+ .attr('y1', this.yScale_(datum.percentile_100))
+ .attr('y2', this.yScale_(datum.percentile_100));
+ sel.exit().remove();
+
+ // Draw a vertical line between percentiles 0 and 100.
+ sel = boxesSel.data([datum]);
+ sel.enter().append('line')
+ .attr('stroke', color)
+ .attr('x1', this.xScale_(index + 0.5))
+ .attr('x2', this.xScale_(index + 0.5))
+ .attr('y1', this.yScale_(datum.percentile_100))
+ .attr('y2', this.yScale_(datum.percentile_0));
+ sel.exit().remove();
+ }
+ }
+ };
+
+ return {
+ BoxChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html
new file mode 100644
index 00000000000..da2c7665ace
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/box_chart_test.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/box_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.BoxChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {
+ x: 'a'.repeat(15) + 'A',
+ percentile_0: 30,
+ percentile_25: 60,
+ percentile_50: 110,
+ percentile_75: 160,
+ percentile_100: 210,
+ },
+ {
+ x: 'b'.repeat(10) + 'B',
+ percentile_0: 0,
+ percentile_25: 50,
+ percentile_50: 100,
+ percentile_75: 150,
+ percentile_100: 200,
+ },
+ {
+ x: 'c'.repeat(5) + 'C',
+ percentile_0: 100,
+ percentile_25: 150,
+ percentile_50: 200,
+ percentile_75: 250,
+ percentile_100: 300,
+ },
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html b/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html
new file mode 100644
index 00000000000..540f8e1c7ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/camera.html
@@ -0,0 +1,350 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const deg2rad = tr.b.math.deg2rad;
+
+ const constants = {
+ DEFAULT_SCALE: 0.5,
+ DEFAULT_EYE_DISTANCE: 10000,
+ MINIMUM_DISTANCE: 1000,
+ MAXIMUM_DISTANCE: 100000,
+ FOV: 15,
+ RESCALE_TIMEOUT_MS: 200,
+ MAXIMUM_TILT: 80,
+ SETTINGS_NAMESPACE: 'tr.ui_camera'
+ };
+
+ const Camera = tr.ui.b.define('camera');
+
+ Camera.prototype = {
+ __proto__: HTMLUnknownElement.prototype,
+
+ decorate(eventSource) {
+ this.eventSource_ = eventSource;
+
+ this.eventSource_.addEventListener('beginpan',
+ this.onPanBegin_.bind(this));
+ this.eventSource_.addEventListener('updatepan',
+ this.onPanUpdate_.bind(this));
+ this.eventSource_.addEventListener('endpan',
+ this.onPanEnd_.bind(this));
+
+ this.eventSource_.addEventListener('beginzoom',
+ this.onZoomBegin_.bind(this));
+ this.eventSource_.addEventListener('updatezoom',
+ this.onZoomUpdate_.bind(this));
+ this.eventSource_.addEventListener('endzoom',
+ this.onZoomEnd_.bind(this));
+
+ this.eventSource_.addEventListener('beginrotate',
+ this.onRotateBegin_.bind(this));
+ this.eventSource_.addEventListener('updaterotate',
+ this.onRotateUpdate_.bind(this));
+ this.eventSource_.addEventListener('endrotate',
+ this.onRotateEnd_.bind(this));
+
+ this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE];
+ this.gazeTarget_ = [0, 0, 0];
+ this.rotation_ = [0, 0];
+
+ this.pixelRatio_ = window.devicePixelRatio || 1;
+ },
+
+
+ get modelViewMatrix() {
+ const mvMatrix = mat4.create();
+
+ mat4.lookAt(mvMatrix, this.eye_, this.gazeTarget_, [0, 1, 0]);
+ return mvMatrix;
+ },
+
+ get projectionMatrix() {
+ const rect =
+ tr.ui.b.windowRectForElement(this.canvas_).
+ scaleSize(this.pixelRatio_);
+
+ const aspectRatio = rect.width / rect.height;
+ const matrix = mat4.create();
+ mat4.perspective(
+ matrix, deg2rad(constants.FOV), aspectRatio, 1, 100000);
+
+ return matrix;
+ },
+
+ set canvas(c) {
+ this.canvas_ = c;
+ },
+
+ set deviceRect(rect) {
+ this.deviceRect_ = rect;
+ },
+
+ get stackingDistanceDampening() {
+ const gazeVector = [
+ this.gazeTarget_[0] - this.eye_[0],
+ this.gazeTarget_[1] - this.eye_[1],
+ this.gazeTarget_[2] - this.eye_[2]];
+ vec3.normalize(gazeVector, gazeVector);
+ return 1 + gazeVector[2];
+ },
+
+ loadCameraFromSettings(settings) {
+ this.eye_ = settings.get(
+ 'eye', this.eye_, constants.SETTINGS_NAMESPACE);
+ this.gazeTarget_ = settings.get(
+ 'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE);
+ this.rotation_ = settings.get(
+ 'rotation', this.rotation_, constants.SETTINGS_NAMESPACE);
+
+ this.dispatchRenderEvent_();
+ },
+
+ saveCameraToSettings(settings) {
+ settings.set(
+ 'eye', this.eye_, constants.SETTINGS_NAMESPACE);
+ settings.set(
+ 'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE);
+ settings.set(
+ 'rotation', this.rotation_, constants.SETTINGS_NAMESPACE);
+ },
+
+ resetCamera() {
+ this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE];
+ this.gazeTarget_ = [0, 0, 0];
+ this.rotation_ = [0, 0];
+
+ const settings = tr.b.SessionSettings();
+ const keys = settings.keys(constants.SETTINGS_NAMESPACE);
+ if (keys.length !== 0) {
+ this.loadCameraFromSettings(settings);
+ return;
+ }
+
+ if (this.deviceRect_) {
+ const rect = tr.ui.b.windowRectForElement(this.canvas_).
+ scaleSize(this.pixelRatio_);
+
+ this.eye_[0] = this.deviceRect_.width / 2;
+ this.eye_[1] = this.deviceRect_.height / 2;
+
+ this.gazeTarget_[0] = this.deviceRect_.width / 2;
+ this.gazeTarget_[1] = this.deviceRect_.height / 2;
+ }
+
+ this.saveCameraToSettings(settings);
+ this.dispatchRenderEvent_();
+ },
+
+ updatePanByDelta(delta) {
+ const rect =
+ tr.ui.b.windowRectForElement(this.canvas_).
+ scaleSize(this.pixelRatio_);
+
+ // Get the eye vector, since we'll be adjusting gazeTarget.
+ const eyeVector = [
+ this.eye_[0] - this.gazeTarget_[0],
+ this.eye_[1] - this.gazeTarget_[1],
+ this.eye_[2] - this.gazeTarget_[2]];
+ const length = vec3.length(eyeVector);
+ vec3.normalize(eyeVector, eyeVector);
+
+ const halfFov = constants.FOV / 2;
+ const multiplier =
+ 2.0 * length * Math.tan(deg2rad(halfFov)) / rect.height;
+
+ // Get the up and right vectors.
+ const up = [0, 1, 0];
+ const rotMatrix = mat4.create();
+ mat4.rotate(
+ rotMatrix, rotMatrix, deg2rad(this.rotation_[1]), [0, 1, 0]);
+ mat4.rotate(
+ rotMatrix, rotMatrix, deg2rad(this.rotation_[0]), [1, 0, 0]);
+ vec3.transformMat4(up, up, rotMatrix);
+
+ const right = [0, 0, 0];
+ vec3.cross(right, eyeVector, up);
+ vec3.normalize(right, right);
+
+ // Update the gaze target.
+ for (let i = 0; i < 3; ++i) {
+ this.gazeTarget_[i] +=
+ delta[0] * multiplier * right[i] - delta[1] * multiplier * up[i];
+
+ this.eye_[i] = this.gazeTarget_[i] + length * eyeVector[i];
+ }
+
+ // If we have some z offset, we need to reposition gazeTarget
+ // to be on the plane z = 0 with normal [0, 0, 1].
+ if (Math.abs(this.gazeTarget_[2]) > 1e-6) {
+ const gazeVector = [-eyeVector[0], -eyeVector[1], -eyeVector[2]];
+ const newLength = tr.b.math.clamp(
+ -this.eye_[2] / gazeVector[2],
+ constants.MINIMUM_DISTANCE,
+ constants.MAXIMUM_DISTANCE);
+
+ for (let i = 0; i < 3; ++i) {
+ this.gazeTarget_[i] = this.eye_[i] + newLength * gazeVector[i];
+ }
+ }
+
+ this.saveCameraToSettings(tr.b.SessionSettings());
+ this.dispatchRenderEvent_();
+ },
+
+ updateZoomByDelta(delta) {
+ let deltaY = delta[1];
+ deltaY = tr.b.math.clamp(deltaY, -50, 50);
+ let scale = 1.0 - deltaY / 100.0;
+
+ const eyeVector = [0, 0, 0];
+ vec3.subtract(eyeVector, this.eye_, this.gazeTarget_);
+
+ const length = vec3.length(eyeVector);
+
+ // Clamp the length to allowed values by changing the scale.
+ if (length * scale < constants.MINIMUM_DISTANCE) {
+ scale = constants.MINIMUM_DISTANCE / length;
+ } else if (length * scale > constants.MAXIMUM_DISTANCE) {
+ scale = constants.MAXIMUM_DISTANCE / length;
+ }
+
+ vec3.scale(eyeVector, eyeVector, scale);
+ vec3.add(this.eye_, this.gazeTarget_, eyeVector);
+
+ this.saveCameraToSettings(tr.b.SessionSettings());
+ this.dispatchRenderEvent_();
+ },
+
+ updateRotateByDelta(delta) {
+ delta[0] *= 0.5;
+ delta[1] *= 0.5;
+
+ if (Math.abs(this.rotation_[0] + delta[1]) > constants.MAXIMUM_TILT) {
+ return;
+ }
+ if (Math.abs(this.rotation_[1] - delta[0]) > constants.MAXIMUM_TILT) {
+ return;
+ }
+
+ const eyeVector = [0, 0, 0, 0];
+ vec3.subtract(eyeVector, this.eye_, this.gazeTarget_);
+
+ // Undo the current rotation.
+ const rotMatrix = mat4.create();
+ mat4.rotate(
+ rotMatrix, rotMatrix, -deg2rad(this.rotation_[0]), [1, 0, 0]);
+ mat4.rotate(
+ rotMatrix, rotMatrix, -deg2rad(this.rotation_[1]), [0, 1, 0]);
+ vec4.transformMat4(eyeVector, eyeVector, rotMatrix);
+
+ // Update rotation values.
+ this.rotation_[0] += delta[1];
+ this.rotation_[1] -= delta[0];
+
+ // Redo the new rotation.
+ mat4.identity(rotMatrix);
+ mat4.rotate(
+ rotMatrix, rotMatrix, deg2rad(this.rotation_[1]), [0, 1, 0]);
+ mat4.rotate(
+ rotMatrix, rotMatrix, deg2rad(this.rotation_[0]), [1, 0, 0]);
+ vec4.transformMat4(eyeVector, eyeVector, rotMatrix);
+
+ vec3.add(this.eye_, this.gazeTarget_, eyeVector);
+
+ this.saveCameraToSettings(tr.b.SessionSettings());
+ this.dispatchRenderEvent_();
+ },
+
+
+ // Event callbacks.
+ onPanBegin_(e) {
+ this.panning_ = true;
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ },
+
+ onPanUpdate_(e) {
+ if (!this.panning_) return;
+
+ const delta = this.getMouseDelta_(e, this.lastMousePosition_);
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ this.updatePanByDelta(delta);
+ },
+
+ onPanEnd_(e) {
+ this.panning_ = false;
+ },
+
+ onZoomBegin_(e) {
+ this.zooming_ = true;
+
+ const p = this.getMousePosition_(e);
+
+ this.lastMousePosition_ = p;
+ this.zoomPoint_ = p;
+ },
+
+ onZoomUpdate_(e) {
+ if (!this.zooming_) return;
+
+ const delta = this.getMouseDelta_(e, this.lastMousePosition_);
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ this.updateZoomByDelta(delta);
+ },
+
+ onZoomEnd_(e) {
+ this.zooming_ = false;
+ this.zoomPoint_ = undefined;
+ },
+
+ onRotateBegin_(e) {
+ this.rotating_ = true;
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ },
+
+ onRotateUpdate_(e) {
+ if (!this.rotating_) return;
+
+ const delta = this.getMouseDelta_(e, this.lastMousePosition_);
+ this.lastMousePosition_ = this.getMousePosition_(e);
+ this.updateRotateByDelta(delta);
+ },
+
+ onRotateEnd_(e) {
+ this.rotating_ = false;
+ },
+
+
+ // Misc helper functions.
+ getMousePosition_(e) {
+ const rect = tr.ui.b.windowRectForElement(this.canvas_);
+ return [(e.clientX - rect.x) * this.pixelRatio_,
+ (e.clientY - rect.y) * this.pixelRatio_];
+ },
+
+ getMouseDelta_(e, p) {
+ const newP = this.getMousePosition_(e);
+ return [newP[0] - p[0], newP[1] - p[1]];
+ },
+
+ dispatchRenderEvent_() {
+ tr.b.dispatchSimpleEvent(this, 'renderrequired', false, false);
+ }
+ };
+
+ return {
+ Camera,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html
new file mode 100644
index 00000000000..7083856c539
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/camera_test.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/math/bbox2.html">
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/ui/base/quad_stack_view.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createQuads() {
+ const quads = [
+ tr.b.math.Quad.fromXYWH(-500, -500, 30, 30), // 4 corners
+ tr.b.math.Quad.fromXYWH(-500, 470, 30, 30),
+ tr.b.math.Quad.fromXYWH(470, -500, 30, 30),
+ tr.b.math.Quad.fromXYWH(470, 470, 30, 30),
+ tr.b.math.Quad.fromXYWH(-250, -250, 250, 250), // crosshairs
+ tr.b.math.Quad.fromXYWH(0, -250, 250, 250), // crosshairs
+ tr.b.math.Quad.fromXYWH(-250, 0, 250, 250), // crosshairs
+ tr.b.math.Quad.fromXYWH(0, 0, 250, 250) // crosshairs
+ ];
+ quads[0].stackingGroupId = 0;
+ quads[1].stackingGroupId = 0;
+ quads[2].stackingGroupId = 0;
+ quads[3].stackingGroupId = 0;
+ quads[4].stackingGroupId = 1;
+ quads[5].stackingGroupId = 1;
+ quads[6].stackingGroupId = 1;
+ quads[7].stackingGroupId = 1;
+ return quads;
+ }
+
+ function createQuadStackView(testFramework) {
+ const quads = createQuads();
+ const view = new tr.ui.b.QuadStackView();
+ // simulate the constraints of the layer-tree-view
+ view.style.height = '400px';
+ view.style.width = '800px';
+ view.deviceRect = tr.b.math.Rect.fromXYWH(-250, -250, 500, 500);
+ view.quads = quads;
+
+ testFramework.addHTMLOutput(view);
+ return view;
+ }
+
+ test('initialState', function() {
+ const view = createQuadStackView(this);
+
+ const viewRect =
+ view.getBoundingClientRect();
+ assert.strictEqual(viewRect.height, 400);
+ assert.strictEqual(viewRect.width, 800);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html
new file mode 100644
index 00000000000..3d1359fe61b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base.html
@@ -0,0 +1,453 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/chart_legend_key.html">
+<link rel="import" href="/tracing/ui/base/d3.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<template id="chart-base-template">
+ <svg> <!-- svg tag is dropped by ChartBase.decorate. -->
+ <g xmlns="http://www.w3.org/2000/svg" id="chart-area">
+ <g class="x axis"></g>
+ <g class="y axis"></g>
+ <text id="title"></text>
+ </g>
+ </svg>
+</template>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const DataSeriesEnableChangeEventType = 'data-series-enabled-change';
+
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ const svgNS = 'http://www.w3.org/2000/svg';
+ const ColorScheme = tr.b.ColorScheme;
+
+ function getColorOfKey(key, selected) {
+ let id = ColorScheme.getColorIdForGeneralPurposeString(key);
+ if (selected) {
+ id += ColorScheme.properties.brightenedOffsets[0];
+ }
+ return ColorScheme.colorsAsStrings[id];
+ }
+
+ /**
+ * Returns width and height of SVG text node.
+ *
+ * @param {!Element} parentNode
+ * @param {string} text
+ * @param {function(!Element)=} opt_callback
+ * @param {*=} opt_this
+ * @returns {!Object}
+ */
+ function getSVGTextSize(parentNode, text, opt_callback, opt_this) {
+ const textNode = document.createElementNS(
+ 'http://www.w3.org/2000/svg', 'text');
+ textNode.setAttributeNS(null, 'x', 0);
+ textNode.setAttributeNS(null, 'y', 0);
+ textNode.setAttributeNS(null, 'fill', 'black');
+ textNode.appendChild(document.createTextNode(text));
+ parentNode.appendChild(textNode);
+ if (opt_callback) {
+ opt_callback.call(opt_this || parentNode, textNode);
+ }
+ const width = textNode.getComputedTextLength();
+ const height = textNode.getBBox().height;
+ parentNode.removeChild(textNode);
+ return {width, height};
+ }
+
+ function DataSeries(key) {
+ this.key_ = key;
+ this.target_ = undefined;
+ this.title_ = '';
+ this.optional_ = false;
+ this.enabled_ = true;
+ this.color_ = getColorOfKey(key, false);
+ this.highlightedColor_ = getColorOfKey(key, true);
+ }
+
+ DataSeries.prototype = {
+ get key() {
+ return this.key_;
+ },
+
+ get title() {
+ return this.title_;
+ },
+
+ set title(t) {
+ this.title_ = t;
+ },
+
+ get color() {
+ return this.color_;
+ },
+
+ set color(c) {
+ this.color_ = c;
+ },
+
+ get highlightedColor() {
+ return this.highlightedColor_;
+ },
+
+ set highlightedColor(c) {
+ this.highlightedColor_ = c;
+ },
+
+ get optional() {
+ return this.optional_;
+ },
+
+ set optional(optional) {
+ this.optional_ = optional;
+ },
+
+ get enabled() {
+ return this.enabled_;
+ },
+
+ set enabled(enabled) {
+ // If the caller is disabling a data series, but it wasn't optional, then
+ // force it to be optional.
+ if (!this.optional && !enabled) {
+ this.optional = true;
+ }
+ this.enabled_ = enabled;
+ },
+
+ get target() {
+ return this.target_;
+ },
+
+ set target(t) {
+ this.target_ = t;
+ }
+ };
+
+ /**
+ * A virtual base class for basic charts that provides basic chart
+ * infrastructure such as a title and legend.
+ *
+ * Generally, setting a field on a chart instance will cause it to update its
+ * contents, which assumes that the chart is attached to a document, so
+ * callers should create the chart and immediately attach it to a document
+ * before configuring it. Embedders that are polymer dom-modules can use the
+ * attached() callback to wait to configure the chart until they are attached
+ * to a document.
+ *
+ * TODO(#3058) Use a class for Polymer 2.0.
+ *
+ * @constructor
+ */
+ const ChartBase = tr.ui.b.define('svg', undefined, svgNS);
+
+ ChartBase.prototype = {
+ __proto__: HTMLUnknownElement.prototype,
+
+ getDataSeries(key) {
+ if (!this.seriesByKey_.has(key)) {
+ this.seriesByKey_.set(key, new DataSeries(key));
+ }
+ return this.seriesByKey_.get(key);
+ },
+
+ decorate() {
+ Polymer.dom(this).classList.add('chart-base');
+ this.setAttribute('style', 'cursor: default; user-select: none;');
+ this.chartTitle_ = undefined;
+ this.seriesByKey_ = new Map();
+ this.graphWidth_ = undefined;
+ this.graphHeight_ = undefined;
+ this.margin = {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ };
+ this.hideLegend_ = false;
+ this.showTitleInLegend_ = false;
+ this.titleHeight_ = '16pt';
+
+ // This should use tr.ui.b.instantiateTemplate. However, creating
+ // svg-namespaced elements inside a template isn't possible. Thus, this
+ // hack.
+ const template =
+ Polymer.dom(THIS_DOC).querySelector('#chart-base-template');
+ const svgEl = Polymer.dom(template.content).querySelector('svg');
+ for (let i = 0; i < Polymer.dom(svgEl).children.length; i++) {
+ Polymer.dom(this).appendChild(
+ Polymer.dom(svgEl.children[i]).cloneNode(true));
+ }
+
+ this.addEventListener(DataSeriesEnableChangeEventType,
+ this.onDataSeriesEnableChange_.bind(this));
+ },
+
+ get hideLegend() {
+ return this.hideLegend_;
+ },
+
+ set hideLegend(h) {
+ this.hideLegend_ = h;
+ this.updateContents_();
+ },
+
+ get showTitleInLegend() {
+ return this.showTitleInLegend_;
+ },
+
+ set showTitleInLegend(s) {
+ this.showTitleInLegend_ = s;
+ this.updateContents_();
+ },
+
+ isSeriesEnabled(key) {
+ return this.getDataSeries(key).enabled;
+ },
+
+ onDataSeriesEnableChange_(event) {
+ this.getDataSeries(event.key).enabled = event.enabled;
+ this.updateContents_();
+ },
+
+ get chartTitle() {
+ return this.chartTitle_;
+ },
+
+ set chartTitle(chartTitle) {
+ this.chartTitle_ = chartTitle;
+ this.updateContents_();
+ },
+
+ get chartAreaElement() {
+ return Polymer.dom(this).querySelector('#chart-area');
+ },
+
+ get graphWidth() {
+ if (this.graphWidth_ === undefined) return this.defaultGraphWidth;
+ return this.graphWidth_;
+ },
+
+ set graphWidth(width) {
+ this.graphWidth_ = width;
+ this.updateContents_();
+ },
+
+ get defaultGraphWidth() {
+ return 0;
+ },
+
+ get graphHeight() {
+ if (this.graphHeight_ === undefined) return this.defaultGraphHeight;
+ return this.graphHeight_;
+ },
+
+ set graphHeight(height) {
+ this.graphHeight_ = height;
+ this.updateContents_();
+ },
+
+ get titleHeight() {
+ return this.titleHeight_;
+ },
+
+ set titleHeight(height) {
+ this.titleHeight_ = height;
+ this.updateContents_();
+ },
+
+ get defaultGraphHeight() {
+ return 0;
+ },
+
+ get totalWidth() {
+ return this.margin.left + this.graphWidth + this.margin.right;
+ },
+
+ get totalHeight() {
+ return this.margin.top + this.graphHeight + this.margin.bottom;
+ },
+
+ updateMargins_() {
+ const legendSize = this.computeLegendSize_();
+ this.margin.right = Math.max(this.margin.right, legendSize.width);
+ this.margin.bottom = Math.max(
+ this.margin.bottom,
+ legendSize.height - this.graphHeight);
+
+ if (this.chartTitle_) {
+ const titleSize = getSVGTextSize(this, this.chartTitle_, textNode => {
+ textNode.style.fontSize = '16pt';
+ });
+ this.margin.top = Math.max(this.margin.top, titleSize.height + 15);
+ const horizontalOverhangPx = (titleSize.width - this.graphWidth) / 2;
+ this.margin.left = Math.max(this.margin.left, horizontalOverhangPx);
+ this.margin.right = Math.max(this.margin.right, horizontalOverhangPx);
+ }
+ },
+
+ computeLegendSize_() {
+ let width = 0;
+ let height = 0;
+ if (this.hideLegend) return {width, height};
+
+ let series = [...this.seriesByKey_.values()];
+ if (this.showTitleInLegend) {
+ series = series.filter(series => series.title !== '');
+ }
+
+ for (const seriesEntry of series) {
+ const legendText = this.showTitleInLegend ? seriesEntry.title :
+ seriesEntry.key;
+ const textSize = getSVGTextSize(this, legendText);
+ width = Math.max(width, textSize.width + 30);
+ height += textSize.height;
+ }
+
+ return {width, height};
+ },
+
+ updateDimensions_() {
+ const thisSel = d3.select(this);
+ thisSel.attr('width', this.totalWidth);
+ thisSel.attr('height', this.totalHeight);
+
+ d3.select(this.chartAreaElement).attr(
+ 'transform',
+ 'translate(' + this.margin.left + ', ' + this.margin.top + ')');
+ },
+
+ updateContents_() {
+ this.updateMargins_();
+ this.updateDimensions_();
+ this.updateTitle_();
+ this.updateLegend_();
+ },
+
+ updateTitle_() {
+ const titleSel = d3.select(this.chartAreaElement).select('#title');
+ if (!this.chartTitle_) {
+ titleSel.style('display', 'none');
+ return;
+ }
+ titleSel.attr('transform', 'translate(' + this.graphWidth * 0.5 + ',-15)')
+ .style('display', undefined)
+ .style('text-anchor', 'middle')
+ .style('font-size', this.titleHeight)
+ .attr('class', 'title')
+ .attr('width', this.graphWidth)
+ .text(this.chartTitle_);
+ },
+
+ updateLegend_() {
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ chartAreaSel.selectAll('.legend').remove();
+ if (this.hideLegend) return;
+
+ let series;
+ let seriesText;
+ if (this.showTitleInLegend) {
+ series = [...this.seriesByKey_.values()].
+ filter(series => series.title !== '').
+ filter(series => series.color !== 'transparent').reverse();
+ seriesText = series => series.title;
+ } else {
+ series = [...this.seriesByKey_.values()].
+ filter(series => series.color !== 'transparent').reverse();
+ seriesText = series => series.key;
+ }
+
+ const legendEntriesSel = chartAreaSel.selectAll('.legend').data(series);
+
+ legendEntriesSel.enter()
+ .append('foreignObject')
+ .attr('class', 'legend')
+ .attr('x', this.graphWidth + 2)
+ .attr('width', this.margin.right)
+ .attr('height', 18)
+ .attr('transform', (series, i) => 'translate(0,' + i * 18 + ')')
+ .append('xhtml:body')
+ .style('margin', 0)
+ .append('tr-ui-b-chart-legend-key')
+ .property('color', series =>
+ ((this.currentHighlightedLegendKey === series.key) ?
+ series.highlightedColor : series.color))
+ .property('width', this.margin.right)
+ .property('target', series => series.target)
+ .property('title', series => series.title)
+ .property('optional', series => series.optional)
+ .property('enabled', series => series.enabled)
+ .text(seriesText);
+ legendEntriesSel.exit().remove();
+ },
+
+ get highlightedLegendKey() {
+ return this.highlightedLegendKey_;
+ },
+
+ set highlightedLegendKey(highlightedLegendKey) {
+ this.highlightedLegendKey_ = highlightedLegendKey;
+ this.updateHighlight_();
+ },
+
+ get currentHighlightedLegendKey() {
+ if (this.tempHighlightedLegendKey_) {
+ return this.tempHighlightedLegendKey_;
+ }
+ return this.highlightedLegendKey_;
+ },
+
+ pushTempHighlightedLegendKey(key) {
+ if (this.tempHighlightedLegendKey_) {
+ throw new Error('push cannot nest');
+ }
+ this.tempHighlightedLegendKey_ = key;
+ this.updateHighlight_();
+ },
+
+ popTempHighlightedLegendKey(key) {
+ if (this.tempHighlightedLegendKey_ !== key) {
+ throw new Error('pop cannot happen');
+ }
+ this.tempHighlightedLegendKey_ = undefined;
+ this.updateHighlight_();
+ },
+
+ updateHighlight_() {
+ // Update label colors.
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ const legendEntriesSel = chartAreaSel.selectAll('.legend');
+ const getDataSeries = chart.getDataSeries.bind(chart);
+ const currentHighlightedLegendKey = chart.currentHighlightedLegendKey;
+ legendEntriesSel.each(function(key) {
+ // NOTE: this = legendEntry
+ const dataSeries = getDataSeries(key);
+ if (key === currentHighlightedLegendKey) {
+ this.style.fill = dataSeries.highlightedColor;
+ this.style.fontWeight = 'bold';
+ } else {
+ this.style.fill = dataSeries.color;
+ this.style.fontWeight = '';
+ }
+ });
+ }
+ };
+
+ return {
+ ChartBase,
+ DataSeriesEnableChangeEventType,
+ getColorOfKey,
+ getSVGTextSize,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html
new file mode 100644
index 00000000000..a17e7b74853
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d.html
@@ -0,0 +1,571 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/chart_base.html">
+<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ // This does not include the tick labels.
+ const D3_Y_AXIS_WIDTH_PX = 9;
+
+ // This includes the tick labels.
+ const D3_X_AXIS_HEIGHT_PX = 23;
+
+ // For charts with log y-axes, the y-axis tick values may need to be sanitized
+ // if the data is zero or negative.
+ function sanitizePower(x, defaultValue) {
+ if (!isNaN(x) && isFinite(x) && (x !== 0)) return x;
+ return defaultValue;
+ }
+
+ const ChartBase2D = tr.ui.b.define('chart-base-2d', tr.ui.b.ChartBase);
+
+ ChartBase2D.prototype = {
+ __proto__: tr.ui.b.ChartBase.prototype,
+
+ decorate() {
+ super.decorate();
+ Polymer.dom(this).classList.add('chart-base-2d');
+
+ this.xScale_ = d3.scale.linear();
+ this.yScale_ = d3.scale.linear();
+ this.isYLogScale_ = false;
+ this.yLogScaleBase_ = 10;
+ this.yLogScaleMin_ = undefined;
+ this.autoDataRange_ = new tr.b.math.Range();
+ this.overrideDataRange_ = undefined;
+ this.hideXAxis_ = false;
+ this.hideYAxis_ = false;
+ this.data_ = [];
+ this.xAxisLabel_ = '';
+ this.yAxisLabel_ = '';
+ this.textHeightPx_ = 0;
+ this.unit_ = undefined;
+
+ d3.select(this.chartAreaElement)
+ .append('g')
+ .attr('id', 'brushes');
+ d3.select(this.chartAreaElement)
+ .append('g')
+ .attr('id', 'series');
+
+ this.addEventListener('mousedown', this.onMouseDown_.bind(this));
+ },
+
+ get yLogScaleBase() {
+ return this.yLogScaleBase_;
+ },
+
+ set yLogScaleBase(b) {
+ this.yLogScaleBase_ = b;
+ },
+
+ get unit() {
+ return this.unit_;
+ },
+
+ set unit(unit) {
+ this.unit_ = unit;
+ this.updateContents_();
+ },
+
+ get xAxisLabel() {
+ return this.xAxisLabel_;
+ },
+
+ set xAxisLabel(label) {
+ this.xAxisLabel_ = label;
+ },
+
+ get yAxisLabel() {
+ return this.yAxisLabel_;
+ },
+
+ set yAxisLabel(label) {
+ this.yAxisLabel_ = label;
+ },
+
+ get hideXAxis() {
+ return this.hideXAxis_;
+ },
+
+ set hideXAxis(h) {
+ this.hideXAxis_ = h;
+ this.updateContents_();
+ },
+
+ get hideYAxis() {
+ return this.hideYAxis_;
+ },
+
+ set hideYAxis(h) {
+ this.hideYAxis_ = h;
+ this.updateContents_();
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ /**
+ * Sets the data array for the object
+ *
+ * @param {Array} data The data. Each element must be an object, with at
+ * least an x property. All other properties become series names in the
+ * chart. The data can be sparse (i.e. every x value does not have to
+ * contain data for every series).
+ */
+ set data(data) {
+ if (data === undefined) {
+ throw new Error('data must be an Array');
+ }
+
+ this.data_ = data;
+ this.updateSeriesKeys_();
+ this.updateDataRange_();
+ this.updateContents_();
+ },
+
+ set isYLogScale(logScale) {
+ if (logScale) {
+ this.yScale_ = d3.scale.log().base(this.yLogScaleBase);
+ } else {
+ this.yScale_ = d3.scale.linear();
+ }
+ this.isYLogScale_ = logScale;
+ },
+
+ getYScaleMin_() {
+ return this.isYLogScale_ ? this.yLogScaleMin_ : 0;
+ },
+
+ getYScaleDomain_(minValue, maxValue) {
+ if (this.overrideDataRange_ !== undefined) {
+ return [this.dataRange.min, this.dataRange.max];
+ }
+ if (this.isYLogScale_) {
+ return [this.getYScaleMin_(), maxValue];
+ }
+ return [Math.min(minValue, this.getYScaleMin_()), maxValue];
+ },
+
+ getSampleWidth_(data, index, leftSide) {
+ let leftIndex;
+ let rightIndex;
+ if (leftSide) {
+ leftIndex = Math.max(index - 1, 0);
+ rightIndex = index;
+ } else {
+ leftIndex = index;
+ rightIndex = Math.min(index + 1, data.length - 1);
+ }
+ const leftWidth = this.getXForDatum_(data[index], index) -
+ this.getXForDatum_(data[leftIndex], leftIndex);
+ const rightWidth = this.getXForDatum_(data[rightIndex], rightIndex) -
+ this.getXForDatum_(data[index], index);
+ return tr.b.math.Statistics.mean([leftWidth, rightWidth]);
+ },
+
+ updateSeriesKeys_() {
+ // Don't clear seriesByKey_; the caller might have put state in it using
+ // getDataSeries() before setting data.
+ this.data_.forEach(function(datum) {
+ Object.keys(datum).forEach(function(key) {
+ if (this.isDatumFieldSeries_(key)) {
+ this.getDataSeries(key);
+ }
+ }, this);
+ }, this);
+ },
+
+ isDatumFieldSeries_(fieldName) {
+ return fieldName !== 'x';
+ },
+
+ getXForDatum_(datum, index) {
+ return datum.x;
+ },
+
+ updateMargins_() {
+ this.margin.left = this.hideYAxis ? 0 : this.yAxisWidth;
+ this.margin.bottom = this.hideXAxis ? 0 : this.xAxisHeight;
+
+ if (this.hideXAxis && !this.hideYAxis) {
+ this.margin.bottom = 10;
+ }
+ if (this.hideYAxis && !this.hideXAxis) {
+ this.margin.left = 10;
+ }
+ this.margin.top = this.hideYAxis ? 0 : 10;
+
+ if (this.yAxisLabel) {
+ this.margin.top += this.textHeightPx_;
+ }
+ if (this.xAxisLabel) {
+ this.margin.right = Math.max(this.margin.right,
+ 16 + tr.ui.b.getSVGTextSize(this, this.xAxisLabel).width);
+ }
+
+ super.updateMargins_();
+ },
+
+ get xAxisHeight() {
+ return D3_X_AXIS_HEIGHT_PX;
+ },
+
+ computeScaleTickWidth_(scale) {
+ if (this.data.length === 0) return 0;
+
+ let tickValues = scale.ticks();
+ let tickFormat = scale.tickFormat();
+
+ if (this.isYLogScale_) {
+ const enclosingPowers = this.dataRange.enclosingPowers();
+ tickValues = [];
+ const maxPower = sanitizePower(enclosingPowers.max, this.yLogScaleBase);
+ for (let power = sanitizePower(enclosingPowers.min, 1);
+ power <= maxPower;
+ power *= this.yLogScaleBase) {
+ tickValues.push(power);
+ }
+ tickFormat = v => v.toString();
+ }
+
+ if (this.unit) {
+ tickFormat = v => this.unit.format(v);
+ }
+
+ let maxTickWidth = 0;
+ for (const tickValue of tickValues) {
+ maxTickWidth = Math.max(maxTickWidth,
+ tr.ui.b.getSVGTextSize(this, tickFormat(tickValue)).width);
+ }
+
+ return D3_Y_AXIS_WIDTH_PX + maxTickWidth;
+ },
+
+ get yAxisWidth() {
+ return this.computeScaleTickWidth_(this.yScale_);
+ },
+
+ updateScales_() {
+ if (this.data_.length === 0) return;
+
+ this.xScale_.range([0, this.graphWidth]);
+ this.xScale_.domain(d3.extent(this.data_, this.getXForDatum_.bind(this)));
+
+ this.yScale_.range([this.graphHeight, 0]);
+ this.yScale_.domain([this.dataRange.min, this.dataRange.max]);
+ },
+
+ updateBrushContents_(brushSel) {
+ brushSel.selectAll('*').remove();
+ },
+
+ updateXAxis_(xAxis) {
+ xAxis.selectAll('*').remove();
+ xAxis[0][0].style.opacity = 0;
+ if (this.hideXAxis) return;
+
+ this.drawXAxis_(xAxis);
+
+ const label = xAxis.append('text').attr('class', 'label');
+ this.drawXAxisTicks_(xAxis);
+ this.drawXAxisLabel_(label);
+ xAxis[0][0].style.opacity = 1;
+ },
+
+ drawXAxis_(xAxis) {
+ xAxis.attr('transform', 'translate(0,' + this.graphHeight + ')')
+ .call(d3.svg.axis()
+ .scale(this.xScale_)
+ .orient('bottom'));
+ },
+
+ drawXAxisLabel_(label) {
+ label
+ .attr('x', this.graphWidth + 16)
+ .attr('y', 8)
+ .text(this.xAxisLabel);
+ },
+
+ drawXAxisTicks_(xAxis) {
+ let previousRight = undefined;
+ xAxis.selectAll('.tick')[0].forEach(function(tick) {
+ const currentLeft = tick.transform.baseVal[0].matrix.e;
+ if ((previousRight === undefined) ||
+ (currentLeft > (previousRight + 3))) {
+ const currentWidth = tick.getBBox().width;
+ previousRight = currentLeft + currentWidth;
+ } else {
+ tick.style.opacity = 0;
+ }
+ });
+ },
+
+ set overrideDataRange(range) {
+ this.overrideDataRange_ = range;
+ },
+
+ get dataRange() {
+ if (this.overrideDataRange_ !== undefined) {
+ return this.overrideDataRange_;
+ }
+ return this.autoDataRange_;
+ },
+
+ updateDataRange_() {
+ if (this.overrideDataRange_ !== undefined) return;
+
+ const dataBySeriesKey = this.getDataBySeriesKey_();
+ this.autoDataRange_.reset();
+ for (const [series, values] of Object.entries(dataBySeriesKey)) {
+ for (let i = 0; i < values.length; i++) {
+ this.autoDataRange_.addValue(values[i][series]);
+ }
+ }
+
+ // Choose the closest power of yLogScaleBase, rounded down, as the
+ // smallest tick to display.
+ this.yLogScaleMin_ = undefined;
+ if (this.autoDataRange_.min !== undefined) {
+ let minValue = this.autoDataRange_.min;
+ if (minValue === 0) {
+ minValue = 1;
+ }
+
+ const onePowerLess = tr.b.math.lesserPower(
+ minValue / this.yLogScaleBase);
+ this.yLogScaleMin_ = onePowerLess;
+ }
+ },
+
+ updateYAxis_(yAxis) {
+ yAxis.selectAll('*').remove();
+ yAxis[0][0].style.opacity = 0;
+ if (this.hideYAxis) return;
+
+ this.drawYAxis_(yAxis);
+ this.drawYAxisTicks_(yAxis);
+
+ const label = yAxis.append('text').attr('class', 'label');
+ this.drawYAxisLabel_(label);
+ },
+
+ drawYAxis_(yAxis) {
+ let axisModifier = d3.svg.axis()
+ .scale(this.yScale_)
+ .orient('left');
+
+ let tickFormat;
+
+ if (this.isYLogScale_) {
+ if (this.yLogScaleMin_ === undefined) return;
+ const tickValues = [];
+ const enclosingPowers = this.dataRange.enclosingPowers();
+ const maxPower = sanitizePower(enclosingPowers.max, this.yLogScaleBase);
+ for (let power = sanitizePower(enclosingPowers.min, 1);
+ power <= maxPower;
+ power *= this.yLogScaleBase) {
+ tickValues.push(power);
+ }
+
+ // The default tickFormat() for log scales always uses scientific
+ // notation. Override it to use Number.toString(), which only uses
+ // scientific notation for extreme values, and uses decimal notation for
+ // a broader range of values. Decimal notation is generally slightly
+ // easier to skim than scientific notation in the context of chart axes.
+ axisModifier = axisModifier.tickValues(tickValues);
+ tickFormat = v => v.toString();
+ }
+
+ if (this.unit) {
+ tickFormat = v => this.unit.format(v);
+ }
+
+ if (tickFormat) {
+ axisModifier = axisModifier.tickFormat(tickFormat);
+ }
+
+ yAxis.call(axisModifier);
+ },
+
+ drawYAxisLabel_(label) {
+ const labelWidthPx = Math.ceil(tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, this.yAxisLabel).width);
+ label
+ .attr('x', -labelWidthPx)
+ .attr('y', -8)
+ .text(this.yAxisLabel);
+ },
+
+ drawYAxisTicks_(yAxis) {
+ let previousTop = undefined;
+ yAxis.selectAll('.tick')[0].forEach(function(tick) {
+ const bbox = tick.getBBox();
+ const currentTop = tick.transform.baseVal[0].matrix.f;
+ const currentBottom = currentTop + bbox.height;
+ if ((previousTop === undefined) ||
+ (previousTop > (currentBottom + 3))) {
+ previousTop = currentTop;
+ } else {
+ tick.style.opacity = 0;
+ }
+ });
+ yAxis[0][0].style.opacity = 1;
+ },
+
+ updateContents_() {
+ if (this.textHeightPx_ === 0) {
+ // Measure the height of a string that is as tall as it can be,
+ // with both an ascender and a descender.
+ // https://en.wikipedia.org/wiki/Ascender_(typography)
+ this.textHeightPx_ = tr.ui.b.getSVGTextSize(this, 'Ay').height;
+ // If the chart is not yet rooted in a document, then the height will be
+ // 0. Callers should make sure that updateContents_ is called at least
+ // once after the chart is rooted in a document so that textHeightPx_
+ // can be computed.
+ }
+
+ this.updateScales_();
+ super.updateContents_();
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ this.updateXAxis_(chartAreaSel.select('.x.axis'));
+ this.updateYAxis_(chartAreaSel.select('.y.axis'));
+ for (const child of this.querySelectorAll('.axis path, .axis line')) {
+ child.style.fill = 'none';
+ child.style.shapeRendering = 'crispEdges';
+ child.style.stroke = 'black';
+ }
+ this.updateBrushContents_(chartAreaSel.select('#brushes'));
+ this.updateDataContents_(chartAreaSel.select('#series'));
+ },
+
+ updateDataContents_(seriesSel) {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Returns a map of series key to the data for that series.
+ *
+ * Example:
+ * // returns {y: [{x: 1, y: 1}, {x: 3, y: 3}], z: [{x: 2, z: 2}]}
+ * this.data_ = [{x: 1, y: 1}, {x: 2, z: 2}, {x: 3, y: 3}];
+ * this.getDataBySeriesKey_();
+ * @return {Object} A map of series data by series key.
+ */
+ getDataBySeriesKey_() {
+ const dataBySeriesKey = {};
+ for (const [key, series] of this.seriesByKey_) {
+ dataBySeriesKey[key] = [];
+ }
+
+ this.data_.forEach(function(multiSeriesDatum, index) {
+ const x = this.getXForDatum_(multiSeriesDatum, index);
+
+ d3.keys(multiSeriesDatum).forEach(function(seriesKey) {
+ // Skip 'x' - it's not a series
+ if (seriesKey === 'x') return;
+
+ if (multiSeriesDatum[seriesKey] === undefined) return;
+
+ if (!this.isDatumFieldSeries_(seriesKey)) return;
+
+ const singleSeriesDatum = {x};
+ singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey];
+ dataBySeriesKey[seriesKey].push(singleSeriesDatum);
+ }, this);
+ }, this);
+
+ return dataBySeriesKey;
+ },
+
+ getChartPointAtClientPoint_(clientPoint) {
+ const rect = this.getBoundingClientRect();
+ return {
+ x: clientPoint.x - rect.left - this.margin.left,
+ y: clientPoint.y - rect.top - this.margin.top
+ };
+ },
+
+ getDataPointAtChartPoint_(chartPoint) {
+ return {
+ x: tr.b.math.clamp(this.xScale_.invert(chartPoint.x),
+ this.xScale_.domain()[0], this.xScale_.domain()[1]),
+ y: tr.b.math.clamp(this.yScale_.invert(chartPoint.y),
+ this.yScale_.domain()[0], this.yScale_.domain()[1])
+ };
+ },
+
+ getDataPointAtClientPoint_(clientX, clientY) {
+ const chartPoint = this.getChartPointAtClientPoint_(
+ {x: clientX, y: clientY});
+ return this.getDataPointAtChartPoint_(chartPoint);
+ },
+
+ prepareDataEvent_(mouseEvent, dataEvent) {
+ const dataPoint = this.getDataPointAtClientPoint_(
+ mouseEvent.clientX, mouseEvent.clientY);
+ dataEvent.x = dataPoint.x;
+ dataEvent.y = dataPoint.y;
+ },
+
+ onMouseDown_(mouseEvent) {
+ tr.ui.b.trackMouseMovesUntilMouseUp(
+ this.onMouseMove_.bind(this, mouseEvent.button),
+ this.onMouseUp_.bind(this, mouseEvent.button));
+ mouseEvent.preventDefault();
+ mouseEvent.stopPropagation();
+ const dataEvent = new tr.b.Event('item-mousedown');
+ dataEvent.button = mouseEvent.button;
+ this.prepareDataEvent_(mouseEvent, dataEvent);
+ this.dispatchEvent(dataEvent);
+ for (const child of this.querySelector('#brushes').children) {
+ child.setAttribute('fill', 'rgb(103, 199, 165)');
+ }
+ },
+
+ onMouseMove_(button, mouseEvent) {
+ if (mouseEvent.buttons !== undefined) {
+ mouseEvent.preventDefault();
+ mouseEvent.stopPropagation();
+ }
+ const dataEvent = new tr.b.Event('item-mousemove');
+ dataEvent.button = button;
+ this.prepareDataEvent_(mouseEvent, dataEvent);
+ this.dispatchEvent(dataEvent);
+ for (const child of this.querySelector('#brushes').children) {
+ child.setAttribute('fill', 'rgb(103, 199, 165)');
+ }
+ },
+
+ onMouseUp_(button, mouseEvent) {
+ mouseEvent.preventDefault();
+ mouseEvent.stopPropagation();
+ const dataEvent = new tr.b.Event('item-mouseup');
+ dataEvent.button = button;
+ this.prepareDataEvent_(mouseEvent, dataEvent);
+ this.dispatchEvent(dataEvent);
+ for (const child of this.querySelector('#brushes').children) {
+ child.setAttribute('fill', 'rgb(213, 236, 229)');
+ }
+ }
+ };
+
+ return {
+ ChartBase2D,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html
new file mode 100644
index 00000000000..cba736ee811
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_base_2d_brushable_x.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/chart_base_2d.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const ChartBase2D = tr.ui.b.ChartBase2D;
+ const ChartBase2DBrushX = tr.ui.b.define(
+ 'chart-base-2d-brush-1d', ChartBase2D);
+
+ ChartBase2DBrushX.prototype = {
+ __proto__: ChartBase2D.prototype,
+
+ decorate() {
+ super.decorate();
+ this.brushedRange_ = new tr.b.math.Range();
+ },
+
+ set brushedRange(range) {
+ this.brushedRange_.reset();
+ this.brushedRange_.addRange(range);
+ this.updateContents_();
+ },
+
+ get brushedRange() {
+ return tr.b.math.Range.fromDict(this.brushedRange_.toJSON());
+ },
+
+ computeBrushRangeFromIndices(indexA, indexB) {
+ indexA = tr.b.math.clamp(indexA, 0, this.data_.length - 1);
+ indexB = tr.b.math.clamp(indexB, 0, this.data_.length - 1);
+ const leftIndex = Math.min(indexA, indexB);
+ const rightIndex = Math.max(indexA, indexB);
+
+ const brushRange = new tr.b.math.Range();
+ brushRange.addValue(
+ this.getXForDatum_(this.data_[leftIndex], leftIndex) -
+ this.getSampleWidth_(this.data_, leftIndex, true));
+ brushRange.addValue(
+ this.getXForDatum_(this.data_[rightIndex], rightIndex) +
+ this.getSampleWidth_(this.data_, rightIndex, false));
+ return brushRange;
+ },
+
+ getDataIndex_(dataX) {
+ if (this.data.length === 0) return undefined;
+ const bisect = d3.bisector(this.getXForDatum_.bind(this)).right;
+ return bisect(this.data_, dataX) - 1;
+ },
+
+ prepareDataEvent_(mouseEvent, dataEvent) {
+ ChartBase2D.prototype.prepareDataEvent_.call(
+ this, mouseEvent, dataEvent);
+ dataEvent.index = this.getDataIndex_(dataEvent.x);
+ if (dataEvent.index !== undefined) {
+ dataEvent.data = this.data_[dataEvent.index];
+ }
+ },
+
+ updateBrushContents_(brushSel) {
+ brushSel.selectAll('*').remove();
+ const brushes = this.brushedRange_.isEmpty ? [] : [this.brushedRange_];
+ const brushRectsSel = brushSel.selectAll('rect').data(brushes);
+ brushRectsSel.enter().append('rect');
+ brushRectsSel.exit().remove();
+ this.drawBrush_(brushRectsSel);
+ },
+
+ drawBrush_(brushRectsSel) {
+ brushRectsSel
+ .attr('x', d => this.xScale_(d.min))
+ .attr('y', 0)
+ .attr('width', d => this.xScale_(d.max) - this.xScale_(d.min))
+ .attr('height', this.graphHeight)
+ .attr('fill', 'rgb(213, 236, 229)');
+ }
+ };
+
+ return {
+ ChartBase2DBrushX,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html
new file mode 100644
index 00000000000..1b5b4943898
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/chart_legend_key.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id="tr-ui-b-chart-legend-key">
+ <template>
+ <style>
+ #checkbox {
+ margin: 0;
+ visibility: hidden;
+ vertical-align: text-top;
+ }
+ #label, #link {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ display: inline-block;
+ }
+ </style>
+
+ <input type=checkbox id="checkbox" checked>
+ <tr-ui-a-analysis-link id="link"></tr-ui-a-analysis-link>
+ <label id="label"></label>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-chart-legend-key',
+
+ ready() {
+ this.$.checkbox.addEventListener(
+ 'change', this.onCheckboxChange_.bind(this));
+ },
+
+ /**
+ * Dispatch an event when the checkbox is toggled.
+ * The checkbox is visible when optional is set to true.
+ */
+ onCheckboxChange_() {
+ tr.b.dispatchSimpleEvent(this, tr.ui.b.DataSeriesEnableChangeEventType,
+ true, false,
+ {key: Polymer.dom(this).textContent, enabled: this.enabled});
+ },
+
+ set textContent(t) {
+ Polymer.dom(this.$.label).textContent = t;
+ Polymer.dom(this.$.link).textContent = t;
+ this.updateContents_();
+ },
+
+ set width(w) {
+ w -= 20; // reserve 20px for the checkbox
+ this.$.link.style.width = w + 'px';
+ this.$.label.style.width = w + 'px';
+ },
+
+ get textContent() {
+ return Polymer.dom(this.$.label).textContent;
+ },
+
+ /**
+ * When a legend-key is "optional", then its checkbox is visible to allow
+ * the user to enable/disable the data series for the key.
+ *
+ * @param {boolean} optional
+ */
+ set optional(optional) {
+ this.$.checkbox.style.visibility = optional ? 'visible' : 'hidden';
+ },
+
+ get optional() {
+ return this.$.checkbox.style.visibility === 'visible';
+ },
+
+ set enabled(enabled) {
+ this.$.checkbox.checked = enabled ? 'checked' : '';
+ },
+
+ get enabled() {
+ return this.$.checkbox.checked;
+ },
+
+ set color(c) {
+ this.$.label.style.color = c;
+ this.$.link.color = c;
+ },
+
+ /**
+ * When target is defined, label is hidden and link is shown.
+ * When the link is clicked, then a RequestSelectionChangeEvent is
+ * dispatched containing the target.
+ * When target is undefined, label is shown and link is hidden, so that the
+ * link is not clickable.
+ */
+ set target(target) {
+ this.$.link.setSelectionAndContent(
+ target, Polymer.dom(this.$.label).textContent);
+ this.updateContents_();
+ },
+
+ get target() {
+ return this.$.link.selection;
+ },
+
+ set title(title) {
+ this.$.link.title = title;
+ },
+
+ updateContents_() {
+ this.$.link.style.display = this.target ? '' : 'none';
+ this.$.label.style.display = this.target ? 'none' : '';
+ this.$.label.htmlFor = this.optional ? 'checkbox' : '';
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html
new file mode 100644
index 00000000000..0de935ed327
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-b-checkbox'>
+ <template>
+ <style>
+ .inline {
+ display: inline-block;
+ }
+ </style>
+
+ <input type="checkbox" id="checkbox" class="inline"/>
+ <div id="label" class="inline"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-checkbox',
+
+ created() {
+ this.needsInit_ = true;
+ this.defaultCheckedValue_ = undefined;
+ this.settingsKey_ = undefined;
+ this.label_ = undefined;
+ this.checked_ = false;
+ this.is_ready_ = false;
+ },
+
+ ready() {
+ this.is_ready_ = true;
+ this.$.checkbox.addEventListener('click', function() {
+ this.checked = this.$.checkbox.checked;
+ }.bind(this));
+ this.maybeUpdateElements_();
+ },
+
+ maybeUpdateElements_() {
+ if (!this.is_ready_) return;
+ this.$.label.innerText = this.label_;
+ this.$.checkbox.checked = this.checked_;
+ },
+
+ get defaultCheckedValue() {
+ return this.defaultCheckedValue_;
+ },
+
+ set defaultCheckedValue(defaultCheckedValue) {
+ if (!this.needsInit_) {
+ throw new Error('Already initialized.');
+ }
+ this.defaultCheckedValue_ = defaultCheckedValue;
+ this.maybeInit_();
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ if (!this.needsInit_) {
+ throw new Error('Already initialized.');
+ }
+ this.settingsKey_ = settingsKey;
+ this.maybeInit_();
+ },
+
+ maybeInit_() {
+ if (!this.needsInit_) return;
+ if (this.settingsKey_ === undefined) return;
+ if (this.defaultCheckedValue_ === undefined) return;
+ this.needsInit_ = false;
+ this.checked = tr.b.Settings.get(
+ this.settingsKey_, this.defaultCheckedValue_);
+ },
+
+ get label() {
+ return this.label_;
+ },
+
+ set label(label) {
+ this.label_ = label;
+ this.maybeUpdateElements_();
+ },
+
+ get checked() {
+ return this.checked_;
+ },
+
+ set checked(checked) {
+ this.checked_ = checked;
+ this.maybeUpdateElements_();
+ tr.b.Settings.set(this.settingsKey_, this.checked_);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html
new file mode 100644
index 00000000000..63073255379
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/checkbox.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-b-checkbox-picker'>
+ <template>
+ <style>
+ #container {
+ display: flex;
+ flex-direction: column;
+ }
+ </style>
+
+ <div id="container">
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-checkbox-picker',
+ created() {
+ this.needsInit_ = true;
+ this.settingsKey_ = undefined;
+ this.is_ready_ = false;
+ this.checkboxes_ = undefined;
+ },
+
+ ready() {
+ this.is_ready_ = true;
+ this.maybeInit_();
+ this.maybeRenderCheckboxes_();
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ if (!this.needsInit_) {
+ throw new Error('Already initialized.');
+ }
+ this.settingsKey_ = settingsKey;
+ this.maybeInit_();
+ },
+
+ maybeInit_() {
+ if (!this.needsInit_) return;
+ if (this.settingsKey_ === undefined) return;
+ if (this.checkboxes_ === undefined) return;
+
+ this.needsInit_ = false;
+
+ for (const key in this.checkboxes_) {
+ this.checkboxes_[key].defaultCheckedValue = false;
+ this.checkboxes_[key].settingsKey = this.settingsKey_ + key;
+ }
+ },
+
+ set items(items) {
+ this.checkboxes_ = {};
+ items.forEach(function(e) {
+ if (e.key in this.checkboxes_) {
+ throw new Error(e.key + ' already exists');
+ }
+ const checkboxEl = document.createElement('tr-ui-b-checkbox');
+ checkboxEl.label = e.label;
+ this.checkboxes_[e.key] = checkboxEl;
+ }.bind(this));
+ this.maybeInit_();
+ this.maybeRenderCheckboxes_();
+ },
+
+ maybeRenderCheckboxes_() {
+ if (!this.is_ready_) return;
+ if (this.checkboxes_ === undefined) return;
+ for (const key in this.checkboxes_) {
+ Polymer.dom(this.$.container).appendChild(this.checkboxes_[key]);
+ }
+ },
+
+ selectCheckbox(key) {
+ if (!(key in this.checkboxes_)) {
+ throw new Error(key + ' does not exists');
+ }
+ this.checkboxes_[key].checked = true;
+ },
+
+ unselectCheckbox(key) {
+ if (!(key in this.checkboxes_)) {
+ throw new Error(key + ' does not exists');
+ }
+ this.checkboxes_[key].checked = false;
+ },
+
+ get checkedKeys() {
+ return Object.keys(this.checkboxes_).filter(function(k) {
+ return this.checkboxes_[k].checked;
+ }.bind(this));
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html
new file mode 100644
index 00000000000..ad893d6cc51
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_picker_test.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/checkbox_picker.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basicAllCheckboxUnchecked', function() {
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'}
+ ];
+ this.addHTMLOutput(cp);
+ assert.deepEqual(cp.checkedKeys, []);
+ });
+
+ test('basicSomeCheckboxChecked', function() {
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+
+ cp.selectCheckbox('Toyota');
+ cp.selectCheckbox('Tesla');
+ this.addHTMLOutput(cp);
+ assert.deepEqual(cp.checkedKeys.sort(), ['Tesla', 'Toyota']);
+ cp.unselectCheckbox('Toyota');
+ assert.deepEqual(cp.checkedKeys, ['Tesla']);
+ });
+
+ test('duplicateKeys', function() {
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ assert.throws(function() {
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Toyota', label: 'I want to drive electric car'},
+ ];
+ });
+ });
+
+ test('selectAndUnselectNonExistingKey', function() {
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ ];
+ assert.throws(function() {
+ cp.selectCheckbox('Lamborghini');
+ });
+ assert.throws(function() {
+ cp.unselectCheckbox('Roll Royce');
+ });
+ });
+
+ test('testPersistentStateOneSetSettingsKeyBeforeSettingItems', function() {
+ const container1 = tr.ui.b.createDiv({textContent: 'Checkbox Picker One'});
+ container1.style.border = 'solid';
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.settingsKey = 'checkbox-picker-test-one';
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+ cp.selectCheckbox('Toyota');
+ cp.selectCheckbox('Tesla');
+ Polymer.dom(container1).appendChild(cp);
+ this.addHTMLOutput(container1);
+ cp.unselectCheckbox('Tesla');
+ assert.deepEqual(cp.checkedKeys, ['Toyota']);
+
+ this.addHTMLOutput(document.createElement('br'));
+
+ const container2 = tr.ui.b.createDiv(
+ {textContent:
+ 'Checkbox Picker Two (Same settingsKey as Checkbox Picker One)'});
+ container2.style.border = 'solid #0000FF';
+ const cp2 = document.createElement('tr-ui-b-checkbox-picker');
+ cp2.settingsKey = 'checkbox-picker-test-one';
+ cp2.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+ Polymer.dom(container2).appendChild(cp2);
+ this.addHTMLOutput(container2);
+ assert.deepEqual(cp2.checkedKeys, ['Toyota']);
+ });
+
+ test('testPersistentStateTwoSetSettingsKeyAfterSettingItems', function() {
+ const container1 = tr.ui.b.createDiv({textContent: 'Checkbox Picker One'});
+ container1.style.border = 'solid';
+ const cp = document.createElement('tr-ui-b-checkbox-picker');
+ cp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+ cp.settingsKey = 'checkbox-picker-test-one';
+ cp.selectCheckbox('Toyota');
+ cp.selectCheckbox('Tesla');
+ Polymer.dom(container1).appendChild(cp);
+ this.addHTMLOutput(container1);
+ assert.deepEqual(cp.checkedKeys.sort(), ['Tesla', 'Toyota']);
+
+ this.addHTMLOutput(document.createElement('br'));
+
+ const container2 = tr.ui.b.createDiv(
+ {textContent:
+ 'Checkbox Picker Two (Same settingsKey as Checkbox Picker One)'});
+ container2.style.border = 'solid #0000FF';
+ const cp2 = document.createElement('tr-ui-b-checkbox-picker');
+ cp2.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Honda', label: 'I want to drive Honda'},
+ {key: 'Tesla', label: 'I want to drive electric car'},
+ ];
+ Polymer.dom(container2).appendChild(cp2);
+ this.addHTMLOutput(container2);
+ cp2.settingsKey = 'checkbox-picker-test-one';
+ assert.deepEqual(cp2.checkedKeys.sort(), ['Tesla', 'Toyota']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html
new file mode 100644
index 00000000000..47ac3492ef5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/checkbox_test.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/checkbox.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basicUnchecked', function() {
+ const checkbox = document.createElement('tr-ui-b-checkbox');
+ checkbox.label = 'Yo like pizza?';
+ this.addHTMLOutput(checkbox);
+ assert.strictEqual(checkbox.label, 'Yo like pizza?');
+ assert.isFalse(checkbox.checked);
+ });
+
+ test('basicChecked', function() {
+ const checkbox = document.createElement('tr-ui-b-checkbox');
+ checkbox.label = 'Yo like cookie?';
+ checkbox.checked = true;
+ this.addHTMLOutput(checkbox);
+ assert.strictEqual(checkbox.label, 'Yo like cookie?');
+ assert.isTrue(checkbox.checked);
+ });
+
+ test('testPersistentStateOneSetSettingsKeyBeforeAddToDom', function() {
+ const checkbox = document.createElement('tr-ui-b-checkbox');
+ checkbox.settingsKey = 'checkbox-basic-test-one';
+ checkbox.label = 'I like sushi';
+ checkbox.defaultCheckedValue = false;
+ this.addHTMLOutput(checkbox);
+ assert.isFalse(checkbox.checked);
+ checkbox.checked = true;
+
+ const checkbox2 = document.createElement('tr-ui-b-checkbox');
+ checkbox2.label = 'I like sushi';
+ checkbox2.defaultCheckedValue = false;
+ checkbox2.settingsKey = 'checkbox-basic-test-one';
+ this.addHTMLOutput(checkbox2);
+ assert.isTrue(checkbox2.checked);
+ });
+
+ test('testPersistentStateTwoSetSettingsKeyAfterAddToDom', function() {
+ const checkbox = document.createElement('tr-ui-b-checkbox');
+ this.addHTMLOutput(checkbox);
+ checkbox.label = 'I like Ramen';
+ checkbox.settingsKey = 'checkbox-basic-test-two';
+ checkbox.defaultCheckedValue = false;
+ assert.isFalse(checkbox.checked);
+ checkbox.checked = true;
+
+ const checkbox2 = document.createElement('tr-ui-b-checkbox');
+ this.addHTMLOutput(checkbox2);
+ checkbox2.label = 'I like Ramen';
+ checkbox2.defaultCheckedValue = false;
+ checkbox2.settingsKey = 'checkbox-basic-test-two';
+ assert.isTrue(checkbox2.checked);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html
new file mode 100644
index 00000000000..c082f12f834
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/model/compound_event_selection_state.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<!--
+@fileoverview A component used to display a label and a color square.
+
+The colored square is typically filled with the color associated with
+that label, using the getColorId* methods from base/color_scheme.
+-->
+<dom-module id='tr-ui-b-color-legend'>
+ <template>
+ <style>
+ :host {
+ display: inline-block;
+ }
+
+ #square {
+ font-size: 150%; /* Make the square bigger. */
+ line-height: 0%; /* Prevent the square from increasing legend height. */
+ }
+ </style>
+ <span id="square"></span>
+ <span id="label"></span>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-color-legend',
+
+ ready() {
+ const blackSquareCharCode = 9632;
+ this.$.square.innerText = String.fromCharCode(blackSquareCharCode);
+ this.label_ = undefined;
+
+ this.compoundEventSelectionState_ =
+ tr.model.CompoundEventSelectionState.NOT_SELECTED;
+ },
+
+ set compoundEventSelectionState(compoundEventSelectionState) {
+ this.compoundEventSelectionState_ = compoundEventSelectionState;
+ // TODO(nduca): Adjust appearance based on associated state.
+ },
+
+ get label() {
+ return this.label_;
+ },
+
+ set label(label) {
+ if (label === undefined) {
+ this.setLabelAndColorId(undefined, undefined);
+ return;
+ }
+
+ const colorId = tr.b.ColorScheme.getColorIdForGeneralPurposeString(
+ label);
+ this.setLabelAndColorId(label, colorId);
+ },
+
+ setLabelAndColorId(label, colorId) {
+ this.label_ = label;
+
+ Polymer.dom(this.$.label).textContent = '';
+ Polymer.dom(this.$.label).appendChild(tr.ui.b.asHTMLOrTextNode(label));
+
+ if (colorId === undefined) {
+ this.$.square.style.color = 'initial';
+ } else {
+ this.$.square.style.color = tr.b.ColorScheme.colorsAsStrings[colorId];
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html
new file mode 100644
index 00000000000..b19b3a4b7e5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/color_legend_test.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/color_legend.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const CompoundEventSelectionState = tr.model.CompoundEventSelectionState;
+
+ function checkSquareColor(colorLegend, expectedColor) {
+ assert.strictEqual(
+ getComputedStyle(colorLegend.$.square).color, expectedColor);
+ }
+
+ test('noLabelSet', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(0, 0, 0)');
+ });
+
+ test('undefinedLabel', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = undefined;
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(0, 0, 0)');
+ });
+
+ test('emptyLabel', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = '';
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(255, 161, 161)');
+ });
+
+ test('nonEmptyLabel', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Frequency';
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(255, 133, 236)');
+ });
+
+ test('longLabel', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Total memory usage';
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(150, 193, 255)');
+ });
+
+ test('directlySetColorId', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.setLabelAndColorId('hello_world', 7 /* colorId */);
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(152, 220, 149)');
+ });
+
+ test('directlyProvidedLabelElement', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.setLabelAndColorId(
+ tr.ui.b.createSpan({textContent: 'hello',
+ className: 'hello-span'}),
+ 7 /* colorId */);
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(152, 220, 149)');
+ });
+
+ test('cessObjectSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Object selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.EVENT_SELECTED;
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(228, 184, 134)');
+ });
+
+ test('cessSomeAssociatedObjectsSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Some associated objects selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.SOME_ASSOCIATED_EVENTS_SELECTED;
+
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(255, 155, 172)');
+ });
+
+ test('cessAllAssociatedObjectsSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'All associated objects selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.ALL_ASSOCIATED_EVENTS_SELECTED;
+
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(150, 193, 255)');
+ });
+
+ test('cessObjectAndSomeAssociatedObjectsSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Object and some associated objects selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.EVENT_AND_SOME_ASSOCIATED_SELECTED;
+
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(204, 158, 255)');
+ });
+
+ test('cessObjectAndAllAssociatedObjectsSelected', function() {
+ const colorLegend = document.createElement('tr-ui-b-color-legend');
+ colorLegend.label = 'Object and all associated objects selected';
+ colorLegend.compoundEventSelectionState =
+ CompoundEventSelectionState.EVENT_AND_ALL_ASSOCIATED_SELECTED;
+
+ this.addHTMLOutput(colorLegend);
+ checkSquareColor(colorLegend, 'rgb(255, 146, 193)');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html
new file mode 100644
index 00000000000..9d14ce92974
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart.html
@@ -0,0 +1,427 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/chart_base_2d_brushable_x.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const ColumnChart = tr.ui.b.define('column-chart', tr.ui.b.ChartBase2DBrushX);
+
+ ColumnChart.prototype = {
+ __proto__: tr.ui.b.ChartBase2DBrushX.prototype,
+
+ decorate() {
+ super.decorate();
+
+ // ColumnChart allows bars to have arbitrary, non-uniform widths. Bars
+ // need not all be the same width. The width of each bar is automatically
+ // computed from the bar's x-coordinate and that of the next bar, which
+ // can not define the width of the last bar. This is the width (in the
+ // xScale's domain (as opposed to the xScale's range (which is measured in
+ // pixels))) of the last bar. When there are at least 2 bars, this is
+ // computed as the average width of the bars. When there is a single bar,
+ // this must default to a non-zero number so that the width of the only
+ // bar will not be zero.
+ this.xCushion_ = 1;
+
+ this.isStacked_ = false;
+ this.isGrouped_ = false;
+
+ this.enableHoverBox = true;
+ this.displayXInHover = false;
+ this.enableToolTip = false;
+
+ this.toolTipCallBack_ = () => {};
+ },
+
+ set toolTipCallBack(callback) {
+ this.toolTipCallBack_ = callback;
+ },
+
+ get toolTipCallBack() {
+ return this.toolTipCallBack_;
+ },
+
+ set isGrouped(grouped) {
+ this.isGrouped_ = grouped;
+ if (grouped) {
+ this.getDataSeries('group').color = 'transparent';
+ }
+ this.updateContents_();
+ },
+
+ get isGrouped() {
+ return this.isGrouped_;
+ },
+
+ set isStacked(stacked) {
+ this.isStacked_ = true;
+ this.updateContents_();
+ },
+
+ get isStacked() {
+ return this.isStacked_;
+ },
+
+ get defaultGraphHeight() {
+ return 100;
+ },
+
+ get defaultGraphWidth() {
+ return 10 * this.data_.length;
+ },
+
+ updateScales_() {
+ if (this.data_.length === 0) return;
+
+ let xDifferences = 0;
+ let currentX = undefined;
+ let previousX = undefined;
+ this.data_.forEach(function(datum, index) {
+ previousX = currentX;
+ currentX = this.getXForDatum_(datum, index);
+ if (previousX !== undefined) {
+ xDifferences += currentX - previousX;
+ }
+ }, this);
+
+ // X.
+ // Leave a cushion on the right so that the last rect doesn't
+ // exceed the chart boundaries. The last rect's width is set to the
+ // average width of the rects, which is chart.width / data.length.
+ this.xScale_.range([0, this.graphWidth]);
+ const domain = d3.extent(this.data_, this.getXForDatum_.bind(this));
+ if (this.data_.length > 1) {
+ this.xCushion_ = xDifferences / (this.data_.length - 1);
+ }
+ this.xScale_.domain([domain[0], domain[1] + this.xCushion_]);
+
+ // Y.
+ this.yScale_.range([this.graphHeight, 0]);
+ this.yScale_.domain(this.getYScaleDomain_(
+ this.dataRange.min, this.dataRange.max));
+ },
+
+ updateDataRange_() {
+ if (!this.isStacked) {
+ super.updateDataRange_();
+ return;
+ }
+
+ this.autoDataRange_.reset();
+ this.autoDataRange_.addValue(0);
+ for (const datum of this.data_) {
+ let sum = 0;
+ for (const [key, series] of this.seriesByKey_) {
+ if (datum[key] === undefined) {
+ continue;
+ } else if (this.isGrouped && key === 'group') {
+ continue;
+ }
+ sum += datum[key];
+ }
+ this.autoDataRange_.addValue(sum);
+ }
+ },
+
+ getStackedRectsForDatum_(datum, index) {
+ const stacks = [];
+ let bottom = this.yScale_.range()[0];
+ let sum = 0;
+ for (const [key, series] of this.seriesByKey_) {
+ if (datum[key] === undefined || !this.isSeriesEnabled(key)) {
+ continue;
+ } else if (this.isGrouped && key === 'group') {
+ continue;
+ }
+
+ sum += this.dataRange.clamp(datum[key]);
+ const heightPx = bottom - this.yScale_(sum);
+ bottom -= heightPx;
+ stacks.push({
+ key,
+ value: datum[key],
+ color: this.getDataSeries(key).color,
+ heightPx,
+ topPx: bottom,
+ underflow: sum < this.dataRange.min,
+ overflow: sum > this.dataRange.max,
+ });
+ }
+ return stacks;
+ },
+
+ getRectsForDatum_(datum, index) {
+ if (this.isStacked) {
+ return this.getStackedRectsForDatum_(datum, index);
+ }
+
+ const stacks = [];
+ for (const [key, series] of this.seriesByKey_) {
+ if (datum[key] === undefined || !this.isSeriesEnabled(key)) {
+ continue;
+ }
+
+ const clampedValue = this.dataRange.clamp(datum[key]);
+ const topPx = this.yScale_(Math.max(
+ clampedValue, this.getYScaleMin_()));
+ stacks.push({
+ key,
+ value: datum[key],
+ topPx,
+ heightPx: this.yScale_.range()[0] - topPx,
+ color: this.getDataSeries(key).color,
+ underflow: datum[key] < this.dataRange.min,
+ overflow: datum[key] > this.dataRange.max,
+ });
+ }
+ stacks.sort(function(a, b) {
+ return b.topPx - a.topPx;
+ });
+ return stacks;
+ },
+
+ drawToolTip_(rect) {
+ if (!this.enableToolTip) return;
+
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ chartAreaSel.selectAll('.tooltip').remove();
+
+ const labelText = 'View Breakdown';
+ const labelWidth = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, labelText).width + 5;
+ const labelHeight = this.textHeightPx_;
+
+ const toolTipLeftPx = rect.leftPx + (rect.widthPx / 2);
+ const toolTipTopPx = rect.topPx;
+
+ chartAreaSel
+ .append('rect')
+ .attr('class', 'tooltip')
+ .attr('fill', 'white')
+ .attr('opacity', 0.8)
+ .attr('stroke', 'black')
+ .attr('x', toolTipLeftPx)
+ .attr('y', toolTipTopPx)
+ .attr('width', labelWidth + 5)
+ .attr('height', labelHeight + 10);
+
+ chartAreaSel
+ .append('text')
+ .style('cursor', 'pointer')
+ .attr('class', 'tooltip')
+ .on('mousedown', () => this.toolTipCallBack_(rect))
+ .attr('fill', 'blue')
+ .attr('x', toolTipLeftPx + 4)
+ .attr('y', toolTipTopPx + labelHeight)
+ .attr('text-decoration', 'underline')
+ .text(labelText);
+ },
+
+ drawHoverValueBox_(rect) {
+ const rectHoverEvent = new tr.b.Event('rect-mouseenter');
+ rectHoverEvent.rect = rect;
+ this.dispatchEvent(rectHoverEvent);
+
+ if (!this.enableHoverBox) return;
+
+ const seriesKeys = [...this.seriesByKey_.keys()];
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ chartAreaSel.selectAll('.hover').remove();
+ let keyWidthPx = 0;
+ let keyHeightPx = 0;
+ if (seriesKeys.length > 1 && !this.isGrouped) {
+ keyWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.key).width + 5;
+ keyHeightPx = this.textHeightPx_;
+ }
+
+ let xLabelWidthPx = 0;
+ let xLabelHeightPx = 0;
+ if (this.displayXInHover) {
+ xLabelWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.datum.x).width + 5;
+ xLabelHeightPx = this.textHeightPx_;
+ }
+
+ let groupWidthPx = 0;
+ let groupHeightPx = 0;
+ if (this.isGrouped && rect.datum.group !== undefined) {
+ groupWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, rect.datum.group).width + 5;
+ groupHeightPx = this.textHeightPx_;
+ }
+
+ let value = rect.value;
+ if (this.unit) value = this.unit.format(value);
+ const valueWidthPx = tr.ui.b.getSVGTextSize(
+ this.chartAreaElement, value).width + 5;
+ const valueHeightPx = this.textHeightPx_;
+
+ const hoverWidthPx = Math.max(keyWidthPx, valueWidthPx,
+ xLabelWidthPx, groupWidthPx);
+
+ let hoverLeftPx = rect.leftPx + (rect.widthPx / 2);
+ hoverLeftPx = Math.max(hoverLeftPx - hoverWidthPx, -this.margin.left);
+
+ const hoverHeightPx = keyHeightPx + valueHeightPx +
+ xLabelHeightPx + groupHeightPx + 2;
+
+ const topOffSetPx = this.isGrouped ? 36 : 12;
+ let hoverTopPx = rect.topPx;
+ hoverTopPx = Math.min(
+ hoverTopPx, this.getBoundingClientRect().height -
+ hoverHeightPx - topOffSetPx);
+
+ chartAreaSel
+ .append('rect')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', 'white')
+ .attr('stroke', 'black')
+ .attr('x', hoverLeftPx)
+ .attr('y', hoverTopPx)
+ .attr('width', hoverWidthPx)
+ .attr('height', hoverHeightPx);
+
+ if (seriesKeys.length > 1 && !this.isGrouped) {
+ chartAreaSel
+ .append('text')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx - 2)
+ .text(rect.key);
+ }
+
+ if (this.displayXInHover) {
+ chartAreaSel.append('text')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx + xLabelHeightPx - 2)
+ .text(rect.datum.x);
+ }
+
+ if (this.isGrouped && rect.datum.group !== undefined) {
+ chartAreaSel.append('text')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + keyHeightPx +
+ xLabelHeightPx + groupHeightPx - 2)
+ .text(rect.datum.group);
+ }
+
+ chartAreaSel
+ .append('text')
+ .attr('class', 'hover')
+ .on('mouseleave', () => this.clearHoverValueBox_(rect))
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .attr('fill', rect.color)
+ .attr('x', hoverLeftPx + 2)
+ .attr('y', hoverTopPx + hoverHeightPx - 2)
+ .text(value);
+ },
+
+ clearHoverValueBox_(rect) {
+ const event = window.event;
+ if (event.relatedTarget &&
+ Array.from(event.relatedTarget.classList).includes('hover')) {
+ return;
+ }
+
+ const rectHoverEvent = new tr.b.Event('rect-mouseleave');
+ rectHoverEvent.rect = rect;
+ this.dispatchEvent(rectHoverEvent);
+
+ d3.select(this.chartAreaElement).selectAll('.hover').remove();
+ },
+
+ drawRect_(rect, sel) {
+ sel = sel.data([rect]);
+ sel.enter().append('rect')
+ .attr('fill', rect.color)
+ .attr('x', rect.leftPx)
+ .attr('y', rect.topPx)
+ .attr('width', rect.widthPx)
+ .attr('height', rect.heightPx)
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .on('mouseenter', this.drawHoverValueBox_.bind(this, rect))
+ .on('mouseleave', this.clearHoverValueBox_.bind(this, rect));
+ sel.exit().remove();
+ },
+
+ drawUnderflow_(rect, sel) {
+ sel = sel.data([rect]);
+ sel.enter().append('text')
+ .text('*')
+ .attr('fill', rect.color)
+ .attr('x', rect.leftPx + (rect.widthPx / 2))
+ .attr('y', this.graphHeight)
+ .on('mousedown', this.drawToolTip_.bind(this, rect))
+ .on('mouseenter', this.drawHoverValueBox_.bind(this, rect))
+ .on('mouseleave', this.clearHoverValueBox_.bind(this, rect));
+ sel.exit().remove();
+ },
+
+ drawOverflow_(rect, sel) {
+ sel = sel.data([rect]);
+ sel.enter().append('text')
+ .text('*')
+ .attr('fill', rect.color)
+ .attr('x', rect.leftPx + (rect.widthPx / 2))
+ .attr('y', 0);
+ sel.exit().remove();
+ },
+
+ updateDataContents_(dataSel) {
+ dataSel.selectAll('*').remove();
+ const chartAreaSel = d3.select(this.chartAreaElement);
+ const seriesKeys = [...this.seriesByKey_.keys()];
+ const rectsSel = dataSel.selectAll('path');
+ this.data_.forEach(function(datum, index) {
+ const currentX = this.getXForDatum_(datum, index);
+ let width = undefined;
+ if (index < (this.data_.length - 1)) {
+ const nextX = this.getXForDatum_(this.data_[index + 1], index + 1);
+ width = nextX - currentX;
+ } else {
+ width = this.xCushion_;
+ }
+ for (const rect of this.getRectsForDatum_(datum, index)) {
+ rect.datum = datum;
+ rect.index = index;
+ rect.leftPx = this.xScale_(currentX);
+ rect.rightPx = this.xScale_(currentX + width);
+ rect.widthPx = rect.rightPx - rect.leftPx;
+ this.drawRect_(rect, rectsSel);
+ if (rect.underflow) {
+ this.drawUnderflow_(rect, rectsSel);
+ }
+ if (rect.overflow) {
+ this.drawOverflow_(rect, rectsSel);
+ }
+ }
+ }, this);
+ }
+ };
+
+ return {
+ ColumnChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html
new file mode 100644
index 00000000000..4771b9f60fa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/column_chart_test.html
@@ -0,0 +1,276 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('chartLegendKey', function() {
+ let key = document.createElement('tr-ui-b-chart-legend-key');
+ key.textContent = 'Lorem ipsum dolor sit amet';
+ key.color = 'red';
+ this.addHTMLOutput(key);
+
+ key = document.createElement('tr-ui-b-chart-legend-key');
+ key.textContent = 'ipsum dolor sit amet';
+ key.target = 'orange ipsum';
+ key.color = 'orange';
+ this.addHTMLOutput(key);
+
+ key = document.createElement('tr-ui-b-chart-legend-key');
+ key.target = 'brown dolor';
+ key.color = 'brown';
+ key.textContent = 'dolor sit amet';
+ this.addHTMLOutput(key);
+ });
+
+ test('instantiation_legendTargets', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.getDataSeries('lorem_ipsum').target = 'lorem_ipsumTarget';
+ chart.getDataSeries('lorem_ipsum').title = 'lorem ipsum';
+ chart.getDataSeries('qux').target = 'quxTarget';
+ chart.getDataSeries('lorem_ipsum').optional = true;
+ chart.getDataSeries('bar').optional = true;
+ chart.isStacked = true;
+ chart.hideXAxis = true;
+ this.addHTMLOutput(chart);
+ chart.data = [{x: 0, foo: 3, lorem_ipsum: 5, bar: 1, qux: 2}];
+
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ chart, function(element) {
+ return element.tagName === 'TR-UI-B-CHART-LEGEND-KEY' &&
+ element.textContent === 'lorem_ipsum' &&
+ element.target === 'lorem_ipsumTarget';
+ }));
+ });
+
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.xAxisLabel = 'ms';
+ chart.yAxisLabel = '#';
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 100},
+ {x: 20, value: 110},
+ {x: 30, value: 100},
+ {x: 40, value: 50}
+ ];
+ });
+
+ test('instantiation_singleDatum', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 100},
+ ];
+ });
+
+ test('instantiation_stacked', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.isStacked = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, foo: 10, bar: 5, qux: 7},
+ {x: 20, foo: 11, bar: 6, qux: 3},
+ {x: 30, foo: 10, bar: 4, qux: 8},
+ {x: 40, foo: 5, bar: 1, qux: 2}
+ ];
+ });
+
+ test('instantiation_singleSeries_yLogScale', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.isYLogScale = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 100},
+ {x: 20, value: 10},
+ {x: 30, value: 1},
+ {x: 40, value: 0.1},
+ {x: 50, value: 0.01},
+ {x: 60, value: 0.001}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50},
+ {x: 20, alpha: 110, beta: 75},
+ {x: 30, alpha: 100, beta: 125},
+ {x: 40, alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSeries_yLogScale', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.isYLogScale = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50},
+ {x: 20, alpha: 110, beta: 75},
+ {x: 30, alpha: 100, beta: 125},
+ {x: 40, alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: undefined},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 25, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: 40},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('brushRangeFromIndices', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ const data = [
+ {x: 10, value: 50},
+ {x: 30, value: 60},
+ {x: 70, value: 70},
+ {x: 80, value: 80},
+ {x: 120, value: 90}
+ ];
+ chart.data = data;
+ let r = new tr.b.math.Range();
+
+ // Range min should be 10.
+ r = chart.computeBrushRangeFromIndices(-2, 1);
+ assert.strictEqual(r.min, 10);
+
+ // Range max should be 120.
+ r = chart.computeBrushRangeFromIndices(3, 10);
+ assert.strictEqual(r.max, 120);
+
+ // Range should be [10, 120]
+ r = chart.computeBrushRangeFromIndices(-2, 10);
+ assert.strictEqual(r.min, 10);
+ assert.strictEqual(r.max, 120);
+
+ // Range should be [20, 100]
+ r = chart.computeBrushRangeFromIndices(1, 3);
+ assert.strictEqual(r.min, 20);
+ assert.strictEqual(r.max, 100);
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 50},
+ {x: 20, value: 60},
+ {x: 30, value: 80},
+ {x: 40, value: 20},
+ {x: 50, value: 30},
+ {x: 60, value: 20},
+ {x: 70, value: 15},
+ {x: 80, value: 20}
+ ];
+
+ let mouseDownX = undefined;
+ let curMouseX = undefined;
+
+ function updateBrushedRange() {
+ if (mouseDownX === undefined || (mouseDownX === curMouseX)) {
+ chart.brushedRange = new tr.b.math.Range();
+ return;
+ }
+ const r = new tr.b.math.Range();
+ r.min = Math.min(mouseDownX, curMouseX);
+ r.max = Math.max(mouseDownX, curMouseX);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownX = e.x;
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ curMouseX = e.x;
+ updateBrushedRange();
+ });
+ });
+
+ test('overrideDataRange', function() {
+ let chart = new tr.ui.b.ColumnChart();
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90);
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 100},
+ ];
+
+ chart = new tr.ui.b.ColumnChart();
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100);
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 50},
+ ];
+ });
+
+ test('instantiationGrouped', function() {
+ const chart = new tr.ui.b.ColumnChart();
+ chart.graphWidth = 300;
+ chart.graphHeight = 200;
+ chart.isStacked = true;
+ chart.isGrouped = true;
+ chart.displayXInHover = true;
+ chart.enableToolTip = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50, group: 'group1' },
+ {x: 20, alpha: 110, beta: 75, group: 'group2' },
+ {x: 30 },
+ {x: 40, alpha: 100, beta: 125, group: 'group1' },
+ {x: 50, alpha: 50, beta: 125, group: 'group2' }
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html b/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html
new file mode 100644
index 00000000000..8c38d0bb1b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/constants.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const constants = {
+ HEADING_WIDTH: 250
+ };
+
+ return {
+ constants,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html
new file mode 100644
index 00000000000..3ce0d3908b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Container that decorates its children.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * @constructor
+ */
+ const ContainerThatDecoratesItsChildren = tr.ui.b.define('div');
+
+ ContainerThatDecoratesItsChildren.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.observer_ = new WebKitMutationObserver(this.didMutate_.bind(this));
+ this.observer_.observe(this, { childList: true });
+
+ // textContent is a variable on regular HTMLElements. However, we want to
+ // hook and prevent writes to it.
+ Object.defineProperty(
+ this, 'textContent',
+ { get: undefined, set: this.onSetTextContent_});
+ },
+
+ appendChild(x) {
+ HTMLDivElement.prototype.appendChild.call(this, x);
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ insertBefore(x, y) {
+ HTMLDivElement.prototype.insertBefore.call(this, x, y);
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ removeChild(x) {
+ HTMLDivElement.prototype.removeChild.call(this, x);
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ replaceChild(x, y) {
+ HTMLDivElement.prototype.replaceChild.call(this, x, y);
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ onSetTextContent_(textContent) {
+ if (textContent !== '') {
+ throw new Error('textContent can only be set to \'\'.');
+ }
+ this.clear();
+ },
+
+ clear() {
+ while (Polymer.dom(this).lastChild) {
+ HTMLDivElement.prototype.removeChild.call(
+ this, Polymer.dom(this).lastChild);
+ }
+ this.didMutate_(this.observer_.takeRecords());
+ },
+
+ didMutate_(records) {
+ this.beginDecorating_();
+ for (let i = 0; i < records.length; i++) {
+ const addedNodes = records[i].addedNodes;
+ if (addedNodes) {
+ for (let j = 0; j < addedNodes.length; j++) {
+ this.decorateChild_(addedNodes[j]);
+ }
+ }
+ const removedNodes = records[i].removedNodes;
+ if (removedNodes) {
+ for (let j = 0; j < removedNodes.length; j++) {
+ this.undecorateChild_(removedNodes[j]);
+ }
+ }
+ }
+ this.doneDecoratingForNow_();
+ },
+
+ decorateChild_(child) {
+ throw new Error('Not implemented');
+ },
+
+ undecorateChild_(child) {
+ throw new Error('Not implemented');
+ },
+
+ beginDecorating_() {
+ },
+
+ doneDecoratingForNow_() {
+ }
+ };
+
+ return {
+ ContainerThatDecoratesItsChildren,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html
new file mode 100644
index 00000000000..54417b3ded6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/container_that_decorates_its_children_test.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createChild() {
+ const span = document.createElement('span');
+ span.decorated = false;
+ return span;
+ }
+
+ /**
+ * @constructor
+ */
+ const SimpleContainer = tr.ui.b.define(
+ 'simple-container', tr.ui.b.ContainerThatDecoratesItsChildren);
+
+ SimpleContainer.prototype = {
+ __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype,
+
+ decorateChild_(child) {
+ assert.isFalse(child.decorated);
+ child.decorated = true;
+ },
+
+ undecorateChild_(child) {
+ assert.isTrue(child.decorated);
+ child.decorated = false;
+ }
+ };
+
+ test('add', function() {
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(createChild());
+ Polymer.dom(container).appendChild(createChild());
+ Polymer.dom(container).appendChild(createChild());
+ assert.isTrue(container.children[0].decorated);
+ assert.isTrue(container.children[1].decorated);
+ assert.isTrue(container.children[2].decorated);
+ });
+
+ test('clearUsingTextContent', function() {
+ const c0 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c0);
+ Polymer.dom(container).textContent = '';
+ assert.isFalse(c0.decorated);
+ });
+
+ test('clear', function() {
+ const c0 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c0);
+ container.clear();
+ assert.isFalse(c0.decorated);
+ });
+
+ test('insertNewBefore', function() {
+ const c0 = createChild();
+ const c1 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c1);
+ Polymer.dom(container).insertBefore(c0, c1);
+ assert.isTrue(c0.decorated);
+ assert.isTrue(c1.decorated);
+ });
+
+ test('insertExistingBefore', function() {
+ const c0 = createChild();
+ const c1 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c1);
+ Polymer.dom(container).appendChild(c0);
+ Polymer.dom(container).insertBefore(c0, c1);
+ assert.isTrue(c0.decorated);
+ assert.isTrue(c1.decorated);
+ });
+
+ test('testReplace', function() {
+ const c0 = createChild();
+ const c1 = createChild();
+ const container = new SimpleContainer();
+ Polymer.dom(container).appendChild(c0);
+ Polymer.dom(container).replaceChild(c1, c0);
+ assert.isFalse(c0.decorated);
+ assert.isTrue(c1.decorated);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html b/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html
new file mode 100644
index 00000000000..bce0554c512
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<script src="/tracing/ui/base/d3_preload.js"></script>
+<script src="/d3.min.js"></script>
+<script src="/tracing/ui/base/d3_postload.js"></script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js
new file mode 100644
index 00000000000..94cefdb15f9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_postload.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2014 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file. */
+'use strict';
+
+(function(window) {
+ window.define = undefined;
+}).call(this, this);
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js
new file mode 100644
index 00000000000..57548f1d175
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/d3_preload.js
@@ -0,0 +1,11 @@
+/* Copyright (c) 2014 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file. */
+'use strict';
+
+(function(window) {
+ window.define = function(x) {
+ window.d3 = x;
+ };
+ window.define.amd = true;
+})(this);
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html
new file mode 100644
index 00000000000..2a1ad88d6cc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function iterateElementDeeplyImpl(element, cb, thisArg, includeElement) {
+ if (includeElement && cb.call(thisArg, element)) return true;
+
+ if (element.root &&
+ element.root !== element &&
+ iterateElementDeeplyImpl(element.root, cb, thisArg, false)) {
+ // Some elements, most notably Polymer template dom-repeat='...'
+ // elements, are their own shadow root. Make sure that we avoid infinite
+ // recursion by avoiding these elements.
+ return true;
+ }
+ const children = Polymer.dom(element).children;
+ for (let i = 0; i < children.length; i++) {
+ if (iterateElementDeeplyImpl(children[i], cb, thisArg, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function iterateElementDeeply(element, cb, thisArg) {
+ iterateElementDeeplyImpl(element, cb, thisArg, false);
+ }
+
+ function findDeepElementMatchingPredicate(element, predicate) {
+ let foundElement = undefined;
+ function matches(element) {
+ const match = predicate(element);
+ if (!match) {
+ return false;
+ }
+ foundElement = element;
+ return true;
+ }
+ iterateElementDeeply(element, matches);
+ return foundElement;
+ }
+
+ function findDeepElementsMatchingPredicate(element, predicate) {
+ const foundElements = [];
+ function matches(element) {
+ const match = predicate(element);
+ if (match) {
+ foundElements.push(element);
+ }
+ return false;
+ }
+ iterateElementDeeply(element, matches);
+ return foundElements;
+ }
+
+ function findDeepElementMatching(element, selector) {
+ return findDeepElementMatchingPredicate(element, function(element) {
+ return element.matches(selector);
+ });
+ }
+ function findDeepElementsMatching(element, selector) {
+ return findDeepElementsMatchingPredicate(element, function(element) {
+ return element.matches(selector);
+ });
+ }
+ function findDeepElementWithTextContent(element, re) {
+ return findDeepElementMatchingPredicate(element, function(element) {
+ if (element.children.length !== 0) return false;
+ return re.test(Polymer.dom(element).textContent);
+ });
+ }
+
+ return {
+ findDeepElementMatching,
+ findDeepElementsMatching,
+ findDeepElementMatchingPredicate,
+ findDeepElementsMatchingPredicate,
+ findDeepElementWithTextContent,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html
new file mode 100644
index 00000000000..d9e1d49f556
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/deep_utils_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+
+<dom-module id='tr-ui-b-deep-utils-test-a'>
+ <template>
+ <div></div>
+ </template>
+</dom-module>
+<dom-module id='tr-ui-b-deep-utils-test-b'>
+ <template>
+ <div></div>
+ </template>
+</dom-module>
+<dom-module id='tr-ui-b-deep-utils-test-c'>
+ <template>
+ <tr-ui-b-deep-utils-test-b class='x'></tr-ui-b-deep-utils-test-b>
+ <tr-ui-b-deep-utils-test-a class='x'></tr-ui-b-deep-utils-test-a>
+ <tr-ui-b-deep-utils-test-a class='x'></tr-ui-b-deep-utils-test-a>
+ </template>
+</dom-module>
+<dom-module id='tr-ui-b-deep-utils-test-d'>
+ <template>
+ <tr-ui-b-deep-utils-test-c></tr-ui-b-deep-utils-test-c>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-deep-utils-test-a'
+});
+
+Polymer({
+ is: 'tr-ui-b-deep-utils-test-b'
+});
+
+Polymer({
+ is: 'tr-ui-b-deep-utils-test-c'
+});
+
+Polymer({
+ is: 'tr-ui-b-deep-utils-test-d'
+});
+
+tr.b.unittest.testSuite(function() {
+ test('testFindDeepElementMatching', function() {
+ const d = document.createElement('tr-ui-b-deep-utils-test-d');
+
+ const b = tr.ui.b.findDeepElementMatching(d, 'tr-ui-b-deep-utils-test-b.x');
+ assert.isDefined(b);
+ assert.strictEqual(b.tagName, 'TR-UI-B-DEEP-UTILS-TEST-B');
+ });
+
+ test('testFindDeepElementsMatching', function() {
+ const d = document.createElement('tr-ui-b-deep-utils-test-d');
+
+ const a = tr.ui.b.findDeepElementsMatching(
+ d, 'tr-ui-b-deep-utils-test-a.x');
+ assert.isDefined(a);
+ assert.strictEqual(a[0].tagName, 'TR-UI-B-DEEP-UTILS-TEST-A');
+ assert.strictEqual(a[1].tagName, 'TR-UI-B-DEEP-UTILS-TEST-A');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html
new file mode 100644
index 00000000000..adf7f10b34d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers.html
@@ -0,0 +1,390 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function createSpan(opt_dictionary) {
+ let ownerDocument = document;
+ if (opt_dictionary && opt_dictionary.ownerDocument) {
+ ownerDocument = opt_dictionary.ownerDocument;
+ }
+ const spanEl = ownerDocument.createElement('span');
+ if (opt_dictionary) {
+ if (opt_dictionary.className) {
+ spanEl.className = opt_dictionary.className;
+ }
+ if (opt_dictionary.textContent) {
+ Polymer.dom(spanEl).textContent =
+ opt_dictionary.textContent;
+ }
+ if (opt_dictionary.tooltip) {
+ spanEl.title = opt_dictionary.tooltip;
+ }
+ if (opt_dictionary.parent) {
+ Polymer.dom(opt_dictionary.parent).appendChild(spanEl);
+ }
+ if (opt_dictionary.bold) {
+ spanEl.style.fontWeight = 'bold';
+ }
+ if (opt_dictionary.italic) {
+ spanEl.style.fontStyle = 'italic';
+ }
+ if (opt_dictionary.marginLeft) {
+ spanEl.style.marginLeft = opt_dictionary.marginLeft;
+ }
+ if (opt_dictionary.marginRight) {
+ spanEl.style.marginRight = opt_dictionary.marginRight;
+ }
+ if (opt_dictionary.backgroundColor) {
+ spanEl.style.backgroundColor = opt_dictionary.backgroundColor;
+ }
+ if (opt_dictionary.color) {
+ spanEl.style.color = opt_dictionary.color;
+ }
+ }
+ return spanEl;
+ }
+
+ function createLink(opt_args) {
+ let ownerDocument = document;
+ if (opt_args && opt_args.ownerDocument) {
+ ownerDocument = opt_args.ownerDocument;
+ }
+ const linkEl = ownerDocument.createElement('a');
+ if (opt_args) {
+ if (opt_args.href) linkEl.href = opt_args.href;
+ if (opt_args.tooltip) linkEl.title = opt_args.tooltip;
+ if (opt_args.color) linkEl.style.color = opt_args.color;
+ if (opt_args.bold) linkEl.style.fontWeight = 'bold';
+ if (opt_args.italic) linkEl.style.fontStyle = 'italic';
+ if (opt_args.className) linkEl.className = opt_args.className;
+ if (opt_args.parent) Polymer.dom(opt_args.parent).appendChild(linkEl);
+ if (opt_args.marginLeft) linkEl.style.marginLeft = opt_args.marginLeft;
+ if (opt_args.marginRight) linkEl.style.marginRight = opt_args.marginRight;
+ if (opt_args.backgroundColor) {
+ linkEl.style.backgroundColor = opt_args.backgroundColor;
+ }
+ if (opt_args.textContent) {
+ Polymer.dom(linkEl).textContent = opt_args.textContent;
+ }
+ }
+ return linkEl;
+ }
+
+ function createDiv(opt_dictionary) {
+ const divEl = document.createElement('div');
+ if (opt_dictionary) {
+ if (opt_dictionary.className) {
+ divEl.className = opt_dictionary.className;
+ }
+ if (opt_dictionary.parent) {
+ Polymer.dom(opt_dictionary.parent).appendChild(divEl);
+ }
+ if (opt_dictionary.textContent) {
+ Polymer.dom(divEl).textContent =
+ opt_dictionary.textContent;
+ }
+ if (opt_dictionary.maxWidth) {
+ divEl.style.maxWidth = opt_dictionary.maxWidth;
+ }
+ }
+ return divEl;
+ }
+
+ function createScopedStyle(styleContent) {
+ const styleEl = document.createElement('style');
+ styleEl.scoped = true;
+ Polymer.dom(styleEl).innerHTML = styleContent;
+ return styleEl;
+ }
+
+ function valuesEqual(a, b) {
+ if (a instanceof Array && b instanceof Array) {
+ return a.length === b.length && JSON.stringify(a) === JSON.stringify(b);
+ }
+ return a === b;
+ }
+
+ function createSelector(
+ targetEl, targetElProperty,
+ settingsKey, defaultValue,
+ items, opt_namespace) {
+ let defaultValueIndex;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (valuesEqual(item.value, defaultValue)) {
+ defaultValueIndex = i;
+ break;
+ }
+ }
+ if (defaultValueIndex === undefined) {
+ throw new Error('defaultValue must be in the items list');
+ }
+
+ const selectorEl = document.createElement('select');
+ selectorEl.addEventListener('change', onChange);
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ const optionEl = document.createElement('option');
+ Polymer.dom(optionEl).textContent = item.label;
+ optionEl.targetPropertyValue = item.value;
+ optionEl.item = item;
+ Polymer.dom(selectorEl).appendChild(optionEl);
+ }
+ function onChange(e) {
+ const value = selectorEl.selectedOptions[0].targetPropertyValue;
+ tr.b.Settings.set(settingsKey, value, opt_namespace);
+ targetEl[targetElProperty] = value;
+ }
+ const oldSetter = targetEl.__lookupSetter__('selectedIndex');
+ selectorEl.__defineGetter__('selectedValue', function(v) {
+ return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue;
+ });
+ selectorEl.__defineGetter__('selectedItem', function(v) {
+ return selectorEl.children[selectorEl.selectedIndex].item;
+ });
+ selectorEl.__defineSetter__('selectedValue', function(v) {
+ for (let i = 0; i < selectorEl.children.length; i++) {
+ const value = selectorEl.children[i].targetPropertyValue;
+ if (valuesEqual(value, v)) {
+ const changed = selectorEl.selectedIndex !== i;
+ if (changed) {
+ selectorEl.selectedIndex = i;
+ onChange();
+ }
+ return;
+ }
+ }
+ throw new Error('Not a valid value');
+ });
+
+ const initialValue = tr.b.Settings.get(
+ settingsKey, defaultValue, opt_namespace);
+ let didSet = false;
+ for (let i = 0; i < selectorEl.children.length; i++) {
+ if (valuesEqual(selectorEl.children[i].targetPropertyValue,
+ initialValue)) {
+ didSet = true;
+ targetEl[targetElProperty] = initialValue;
+ selectorEl.selectedIndex = i;
+ break;
+ }
+ }
+ if (!didSet) {
+ selectorEl.selectedIndex = defaultValueIndex;
+ targetEl[targetElProperty] = defaultValue;
+ }
+
+ return selectorEl;
+ }
+
+ function createEditCategorySpan(optionGroupEl, targetEl) {
+ const spanEl = createSpan({className: 'edit-categories'});
+ Polymer.dom(spanEl).textContent = 'Edit categories';
+ Polymer.dom(spanEl).classList.add('labeled-option');
+
+ spanEl.addEventListener('click', function() {
+ targetEl.onClickEditCategories();
+ });
+ return spanEl;
+ }
+
+ function createOptionGroup(targetEl, targetElProperty,
+ settingsKey, defaultValue,
+ items) {
+ function onChange() {
+ let value = [];
+ if (this.value.length) {
+ value = this.value.split(',');
+ }
+ tr.b.Settings.set(settingsKey, value);
+ targetEl[targetElProperty] = value;
+ }
+
+ const optionGroupEl = createSpan({className: 'labeled-option-group'});
+ const initialValue = tr.b.Settings.get(settingsKey, defaultValue);
+ for (let i = 0; i < items.length; ++i) {
+ const item = items[i];
+ const id = 'category-preset-' + item.label.replace(/ /g, '-');
+
+ const radioEl = document.createElement('input');
+ radioEl.type = 'radio';
+ Polymer.dom(radioEl).setAttribute('id', id);
+ Polymer.dom(radioEl).setAttribute('name', 'category-presets-group');
+ Polymer.dom(radioEl).setAttribute('value', item.value);
+ radioEl.addEventListener('change', onChange.bind(radioEl, targetEl,
+ targetElProperty,
+ settingsKey));
+ if (valuesEqual(initialValue, item.value)) {
+ radioEl.checked = true;
+ }
+
+ const labelEl = document.createElement('label');
+ Polymer.dom(labelEl).textContent = item.label;
+ Polymer.dom(labelEl).setAttribute('for', id);
+
+ const spanEl = createSpan({className: 'labeled-option'});
+ Polymer.dom(spanEl).appendChild(radioEl);
+ Polymer.dom(spanEl).appendChild(labelEl);
+
+ spanEl.__defineSetter__('checked', function(opt_bool) {
+ const changed = radioEl.checked !== (!!opt_bool);
+ if (!changed) return;
+
+ radioEl.checked = !!opt_bool;
+ onChange();
+ });
+ spanEl.__defineGetter__('checked', function() {
+ return radioEl.checked;
+ });
+
+ Polymer.dom(optionGroupEl).appendChild(spanEl);
+ }
+ Polymer.dom(optionGroupEl).appendChild(
+ createEditCategorySpan(optionGroupEl, targetEl));
+ // Since this option group element is not yet added to the tree,
+ // querySelector will fail during updateEditCategoriesStatus_ call.
+ // Hence, creating the element with the 'expanded' classlist category
+ // added, if last selected value was 'Manual' selection.
+ if (!initialValue.length) {
+ Polymer.dom(optionGroupEl).classList.add('categories-expanded');
+ }
+ targetEl[targetElProperty] = initialValue;
+
+ return optionGroupEl;
+ }
+
+ let nextCheckboxId = 1;
+ function createCheckBox(targetEl, targetElProperty,
+ settingsKey, defaultValue,
+ label, opt_changeCb) {
+ const buttonEl = document.createElement('input');
+ buttonEl.type = 'checkbox';
+
+ let initialValue = defaultValue;
+ if (settingsKey !== undefined) {
+ initialValue = tr.b.Settings.get(settingsKey, defaultValue);
+ buttonEl.checked = !!initialValue;
+ }
+ if (targetEl) {
+ targetEl[targetElProperty] = initialValue;
+ }
+
+ function onChange() {
+ if (settingsKey !== undefined) {
+ tr.b.Settings.set(settingsKey, buttonEl.checked);
+ }
+ if (targetEl) {
+ targetEl[targetElProperty] = buttonEl.checked;
+ }
+ if (opt_changeCb) {
+ opt_changeCb.call();
+ }
+ }
+
+ buttonEl.addEventListener('change', onChange);
+
+ const id = '#checkbox-' + nextCheckboxId++;
+
+ const spanEl = createSpan();
+ spanEl.style.display = 'flex';
+ spanEl.style.whiteSpace = 'nowrap';
+ Polymer.dom(buttonEl).setAttribute('id', id);
+
+ const labelEl = document.createElement('label');
+ Polymer.dom(labelEl).textContent = label;
+ Polymer.dom(labelEl).setAttribute('for', id);
+ Polymer.dom(spanEl).appendChild(buttonEl);
+ Polymer.dom(spanEl).appendChild(labelEl);
+
+ spanEl.__defineSetter__('checked', function(opt_bool) {
+ const changed = buttonEl.checked !== (!!opt_bool);
+ if (!changed) return;
+
+ buttonEl.checked = !!opt_bool;
+ onChange();
+ });
+ spanEl.__defineGetter__('checked', function() {
+ return buttonEl.checked;
+ });
+
+ return spanEl;
+ }
+
+ /**
+ * @param {!string} label
+ * @param {function()=} opt_callback
+ * @param {*=} opt_this
+ */
+ function createButton(label, opt_callback, opt_this) {
+ const buttonEl = document.createElement('input');
+ buttonEl.type = 'button';
+ buttonEl.value = label;
+
+ function onClick() {
+ opt_callback.call(opt_this || buttonEl);
+ }
+
+ if (opt_callback) {
+ buttonEl.addEventListener('click', onClick);
+ }
+
+ return buttonEl;
+ }
+
+ function createTextInput(
+ targetEl, targetElProperty, settingsKey, defaultValue) {
+ const initialValue = tr.b.Settings.get(settingsKey, defaultValue);
+ const el = document.createElement('input');
+ el.type = 'text';
+ function onChange(e) {
+ tr.b.Settings.set(settingsKey, el.value);
+ targetEl[targetElProperty] = el.value;
+ }
+ el.addEventListener('input', onChange);
+ el.value = initialValue;
+ targetEl[targetElProperty] = initialValue;
+
+ return el;
+ }
+
+ function isElementAttachedToDocument(el) {
+ let cur = el;
+ while (Polymer.dom(cur).parentNode) {
+ cur = Polymer.dom(cur).parentNode;
+ }
+ return (cur === el.ownerDocument || cur.nodeName === '#document-fragment');
+ }
+
+ function asHTMLOrTextNode(value, opt_ownerDocument) {
+ if (value instanceof Node) {
+ return value;
+ }
+ const ownerDocument = opt_ownerDocument || document;
+ return ownerDocument.createTextNode(value);
+ }
+
+ return {
+ createSpan,
+ createLink,
+ createDiv,
+ createScopedStyle,
+ createSelector,
+ createOptionGroup,
+ createCheckBox,
+ createButton,
+ createTextInput,
+ isElementAttachedToDocument,
+ asHTMLOrTextNode,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html
new file mode 100644
index 00000000000..46313143710
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dom_helpers_test.html
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ test('simpleSpanAndDiv', function() {
+ const divEl = tr.ui.b.createDiv({
+ className: 'a-div-class', parent: document.body
+ });
+ const testText = 'some span text';
+ const spanEl = tr.ui.b.createSpan({
+ className: 'a-span-class',
+ textContent: testText,
+ parent: divEl
+ });
+ const eltInDocument = Polymer.dom(document)
+ .querySelector('.a-div-class>.a-span-class');
+ assert.strictEqual(Polymer.dom(eltInDocument).textContent, testText);
+ Polymer.dom(eltInDocument.parentElement).removeChild(eltInDocument);
+ });
+
+ test('createSpan_ownerDocument', function() {
+ const spanEl = tr.ui.b.createSpan({
+ className: 'a-span-class',
+ bold: true,
+ ownerDocument: THIS_DOC
+ });
+ assert.strictEqual(spanEl.ownerDocument, THIS_DOC);
+ });
+
+ test('createLink', function() {
+ const linkEl = tr.ui.b.createLink({
+ parent: document.body,
+ className: 'a-link-class',
+ textContent: 'Google',
+ href: 'http://www.google.com/'
+ });
+ const eltInDocument = Polymer.dom(document)
+ .querySelector('.a-link-class');
+ assert.strictEqual(Polymer.dom(eltInDocument).textContent, 'Google');
+ assert.strictEqual(eltInDocument.href, 'http://www.google.com/');
+ Polymer.dom(eltInDocument.parentElement).removeChild(eltInDocument);
+ });
+
+ test('checkboxFromDefaults', function() {
+ const target = {foo: undefined};
+ const cb = tr.ui.b.createCheckBox(
+ target, 'foo', 'myCheckBox', false, 'Foo');
+ assert.isFalse(target.foo);
+ });
+
+ test('checkboxFromSettings', function() {
+ tr.b.Settings.set('myCheckBox', true);
+ const target = {foo: undefined};
+ const cb = tr.ui.b.createCheckBox(
+ target, 'foo', 'myCheckBox', false, 'Foo');
+ assert.isTrue(target.foo);
+ });
+
+ test('checkboxChanged', function() {
+ const target = {foo: undefined};
+ const cb = tr.ui.b.createCheckBox(
+ target, 'foo', 'myCheckBox', false, 'Foo');
+ cb.checked = true;
+
+ assert.isTrue(tr.b.Settings.get('myCheckBox', undefined));
+ assert.isTrue(target.foo);
+ });
+
+ test('selectorSettingsAlreaySet', function() {
+ tr.b.Settings.set('myScale', 0.25);
+
+ const target = {
+ scale: 314
+ };
+ const sel = tr.ui.b.createSelector(
+ target, 'scale',
+ 'myScale', 0.375,
+ [{label: '6.25%', value: 0.0625},
+ {label: '12.5%', value: 0.125},
+ {label: '25%', value: 0.25},
+ {label: '37.5%', value: 0.375},
+ {label: '50%', value: 0.5},
+ {label: '75%', value: 0.75},
+ {label: '100%', value: 1},
+ {label: '200%', value: 2}
+ ]);
+ assert.strictEqual(target.scale, 0.25);
+ assert.strictEqual(sel.selectedIndex, 2);
+ });
+
+ test('selectorSettingsDefault', function() {
+ const target = {
+ scale: 314
+ };
+ const sel = tr.ui.b.createSelector(
+ target, 'scale',
+ 'myScale', 0.375,
+ [{label: '6.25%', value: 0.0625},
+ {label: '12.5%', value: 0.125},
+ {label: '25%', value: 0.25},
+ {label: '37.5%', value: 0.375},
+ {label: '50%', value: 0.5},
+ {label: '75%', value: 0.75},
+ {label: '100%', value: 1},
+ {label: '200%', value: 2}
+ ]);
+ assert.strictEqual(target.scale, 0.375);
+ assert.strictEqual(sel.selectedIndex, 3);
+ });
+
+ test('selectorSettingsChanged', function() {
+ const target = {
+ scale: 314
+ };
+ const sel = tr.ui.b.createSelector(
+ target, 'scale',
+ 'myScale', 0.375,
+ [{label: '6.25%', value: 0.0625},
+ {label: '12.5%', value: 0.125},
+ {label: '25%', value: 0.25},
+ {label: '37.5%', value: 0.375},
+ {label: '50%', value: 0.5},
+ {label: '75%', value: 0.75},
+ {label: '100%', value: 1},
+ {label: '200%', value: 2}
+ ]);
+ assert.strictEqual(sel.selectedValue, 0.375);
+ sel.selectedValue = 0.75;
+ assert.strictEqual(target.scale, 0.75);
+ assert.strictEqual(sel.selectedValue, 0.75);
+ assert.strictEqual(undefined), 0.75, tr.b.Settings.get('myScale');
+ });
+
+ test('asHTMLOrTextNode_string', function() {
+ // Default owner document.
+ let node = tr.ui.b.asHTMLOrTextNode('Hello, World!');
+ assert.instanceOf(node, Node);
+ assert.strictEqual(Polymer.dom(node).textContent, 'Hello, World!');
+ assert.strictEqual(node.ownerDocument, document);
+
+ // Custom owner document.
+ node = tr.ui.b.asHTMLOrTextNode('Bye, World!', THIS_DOC);
+ assert.instanceOf(node, Node);
+ assert.strictEqual(Polymer.dom(node).textContent, 'Bye, World!');
+ assert.strictEqual(node.ownerDocument, THIS_DOC);
+ });
+
+ test('asHTMLOrTextNode_node', function() {
+ // Node object. Owner document should NOT be modified.
+ let node = document.createTextNode('Hi', THIS_DOC);
+ assert.strictEqual(tr.ui.b.asHTMLOrTextNode(node), node);
+ assert.strictEqual(node.ownerDocument, document);
+
+ // HTMLElement object. Owner document should NOT be modified.
+ node = THIS_DOC.createElement('div');
+ assert.strictEqual(tr.ui.b.asHTMLOrTextNode(node), node);
+ assert.strictEqual(node.ownerDocument, THIS_DOC);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html
new file mode 100644
index 00000000000..614c3c2fb8f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id="tr-ui-b-drag-handle">
+ <template>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ box-sizing: border-box;
+ display: block;
+ }
+
+ :host(.horizontal-drag-handle) {
+ background-image: -webkit-gradient(linear,
+ 0 0, 0 100%,
+ from(#E5E5E5),
+ to(#D1D1D1));
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ cursor: ns-resize;
+ flex: 0 0 auto;
+ height: 7px;
+ position: relative;
+ }
+
+ :host(.vertical-drag-handle) {
+ background-image: -webkit-gradient(linear,
+ 0 0, 100% 0,
+ from(#E5E5E5),
+ to(#D1D1D1));
+ border-left: 1px solid white;
+ border-right: 1px solid #8e8e8e;
+ cursor: ew-resize;
+ flex: 0 0 auto;
+ position: relative;
+ width: 7px;
+ }
+ </style>
+ <div></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-drag-handle',
+
+ created() {
+ this.lastMousePos_ = 0;
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.onMouseUp_ = this.onMouseUp_.bind(this);
+ this.addEventListener('mousedown', this.onMouseDown_);
+ this.target_ = undefined;
+ this.horizontal = true;
+ this.observer_ = new WebKitMutationObserver(
+ this.didTargetMutate_.bind(this));
+ this.targetSizesByModeKey_ = {};
+ this.currentDraggingSize_ = undefined;
+ },
+
+ get modeKey_() {
+ return this.target_.className === '' ? '.' : this.target_.className;
+ },
+
+ get target() {
+ return this.target_;
+ },
+
+ set target(target) {
+ this.observer_.disconnect();
+ this.target_ = target;
+ if (!this.target_) return;
+ this.observer_.observe(this.target_, {
+ attributes: true,
+ attributeFilter: ['class']
+ });
+ },
+
+ get horizontal() {
+ return this.horizontal_;
+ },
+
+ set horizontal(h) {
+ this.horizontal_ = h;
+ if (this.horizontal_) {
+ this.className = 'horizontal-drag-handle';
+ } else {
+ this.className = 'vertical-drag-handle';
+ }
+ },
+
+ get vertical() {
+ return !this.horizontal_;
+ },
+
+ set vertical(v) {
+ this.horizontal = !v;
+ },
+
+ forceMutationObserverFlush_() {
+ const records = this.observer_.takeRecords();
+ if (records.length) {
+ this.didTargetMutate_(records);
+ }
+ },
+
+ didTargetMutate_(e) {
+ const modeSize = this.targetSizesByModeKey_[this.modeKey_];
+ if (modeSize !== undefined) {
+ this.setTargetSize_(modeSize);
+ return;
+ }
+
+ // If we hadn't previously sized the target, then just remove any manual
+ // sizing that we applied.
+ this.target_.style[this.targetStyleKey_] = '';
+ },
+
+ get targetStyleKey_() {
+ return this.horizontal_ ? 'height' : 'width';
+ },
+
+ getTargetSize_() {
+ // Get the actual size, which may be different from the expected size
+ // because of size constraints (e.g. min-width) etc.
+ const size =
+ parseInt(window.getComputedStyle(this.target_)[this.targetStyleKey_]);
+ this.targetSizesByModeKey_[this.modeKey_] = size;
+ return size;
+ },
+
+ setTargetSize_(s) {
+ this.target_.style[this.targetStyleKey_] = s + 'px';
+ this.targetSizesByModeKey_[this.modeKey_] = this.getTargetSize_();
+ tr.b.dispatchSimpleEvent(this, 'drag-handle-resize', true, false);
+ },
+
+ applyDelta_(delta) {
+ // Apply new size to the target.
+ if (this.target_ === this.nextElementSibling) {
+ this.currentDraggingSize_ += delta;
+ } else {
+ this.currentDraggingSize_ -= delta;
+ }
+ this.setTargetSize_(this.currentDraggingSize_);
+ },
+
+ onMouseMove_(e) {
+ // Compute the difference in height position.
+ const curMousePos = this.horizontal_ ? e.clientY : e.clientX;
+ const delta = this.lastMousePos_ - curMousePos;
+
+ this.applyDelta_(delta);
+
+ this.lastMousePos_ = curMousePos;
+ e.preventDefault();
+ return true;
+ },
+
+ onMouseDown_(e) {
+ if (!this.target_) return;
+ this.forceMutationObserverFlush_();
+ // Start with the current actual size.
+ this.currentDraggingSize_ = this.getTargetSize_();
+ this.lastMousePos_ = this.horizontal_ ? e.clientY : e.clientX;
+ document.addEventListener('mousemove', this.onMouseMove_);
+ document.addEventListener('mouseup', this.onMouseUp_);
+ e.preventDefault();
+ return true;
+ },
+
+ onMouseUp_(e) {
+ document.removeEventListener('mousemove', this.onMouseMove_);
+ document.removeEventListener('mouseup', this.onMouseUp_);
+ e.preventDefault();
+ this.currentDraggingSize_ = undefined;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html
new file mode 100644
index 00000000000..9faa91fce81
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/drag_handle_test.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const createDragHandle = function() {
+ const el = document.createElement('div');
+ el.style.border = '1px solid black';
+ el.style.width = '200px';
+ el.style.height = '200px';
+ el.style.display = 'flex';
+ el.style.flexDirection = 'column';
+
+ const upperEl = document.createElement('div');
+ upperEl.style.flex = '1 1 auto';
+ upperEl.style.minHeight = '0';
+
+ const lowerEl = document.createElement('div');
+ lowerEl.style.height = '100px';
+ lowerEl.style.minHeight = '50px';
+
+ const dragHandle = document.createElement('tr-ui-b-drag-handle');
+ dragHandle.target = lowerEl;
+
+ Polymer.dom(el).appendChild(upperEl);
+ Polymer.dom(el).appendChild(dragHandle);
+ Polymer.dom(el).appendChild(lowerEl);
+ el.upperEl = upperEl;
+ el.dragHandle = dragHandle;
+ el.lowerEl = lowerEl;
+
+ el.getLowerElHeight = function() {
+ return parseInt(getComputedStyle(this.lowerEl).height);
+ };
+ return el;
+ };
+
+ test('instantiate', function() {
+ this.addHTMLOutput(createDragHandle());
+ });
+
+ test('dragWithoutConstraint', function() {
+ const el = createDragHandle();
+ this.addHTMLOutput(el);
+
+ const dragHandle = el.dragHandle;
+ assert.strictEqual(el.getLowerElHeight(), 100);
+ dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}});
+ dragHandle.onMouseMove_({clientX: 0, clientY: -10, preventDefault() {}});
+ assert.strictEqual(el.getLowerElHeight(), 110);
+ dragHandle.onMouseUp_({preventDefault() {}});
+ });
+
+ test('dragWithConstraint', function() {
+ const el = createDragHandle();
+ this.addHTMLOutput(el);
+
+ const dragHandle = el.dragHandle;
+ assert.strictEqual(el.getLowerElHeight(), 100);
+ dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}});
+ dragHandle.onMouseMove_({clientX: 0, clientY: 60, preventDefault() {}});
+ // The actual size is constrained by minHeight.
+ assert.strictEqual(el.getLowerElHeight(), 50);
+ dragHandle.onMouseUp_({preventDefault() {}});
+
+ // Drag again. Should based on the actual size.
+ dragHandle.onMouseDown_({clientX: 0, clientY: 0, preventDefault() {}});
+ dragHandle.onMouseMove_({clientX: 0, clientY: -10, preventDefault() {}});
+ assert.strictEqual(el.getLowerElHeight(), 60);
+ dragHandle.onMouseUp_({preventDefault() {}});
+ });
+
+ test('classNameMutation', function() {
+ const el = createDragHandle();
+
+ const styleEl = document.createElement('style');
+ Polymer.dom(styleEl).textContent =
+ '.mode-a { height: 100px; } .mode-b { height: 50px; }';
+ Polymer.dom(document.head).appendChild(styleEl);
+
+ this.addHTMLOutput(el);
+
+ try {
+ const dragHandle = el.dragHandle;
+ const mouseDown = {clientX: 0, clientY: 0, preventDefault() {}};
+ const mouseMove = {clientX: 0, clientY: -10, preventDefault() {}};
+ const mouseUp = {preventDefault() {}};
+
+ el.lowerEl.className = 'mode-a';
+ assert.strictEqual(el.getLowerElHeight(), 100);
+ dragHandle.onMouseDown_(mouseDown);
+ dragHandle.onMouseMove_(mouseMove);
+ assert.strictEqual(el.getLowerElHeight(), 110);
+ dragHandle.onMouseUp_(mouseUp);
+
+ // Change the class, which should restore the layout
+ // to the default sizing for mode-b
+ el.lowerEl.className = 'mode-b';
+ dragHandle.forceMutationObserverFlush_();
+ assert.strictEqual(el.getLowerElHeight(), 50);
+
+ dragHandle.onMouseDown_(mouseDown);
+ dragHandle.onMouseMove_(mouseMove);
+ assert.strictEqual(el.getLowerElHeight(), 60);
+ dragHandle.onMouseUp_(mouseUp);
+
+ // Restore the class-a, which should restore the layout
+ // to sizing when we were changed.
+ el.lowerEl.className = 'mode-a';
+ dragHandle.forceMutationObserverFlush_();
+ assert.strictEqual(el.getLowerElHeight(), 110);
+
+ dragHandle.onMouseDown_(mouseDown);
+ dragHandle.onMouseMove_(mouseMove);
+ assert.strictEqual(el.getLowerElHeight(), 120);
+ dragHandle.onMouseUp_(mouseUp);
+ } finally {
+ Polymer.dom(document.head).removeChild(styleEl);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html b/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html
new file mode 100644
index 00000000000..449f3576274
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/draw_helpers.html
@@ -0,0 +1,415 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/elided_cache.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides various helper methods for drawing to a provided
+ * canvas.
+ */
+tr.exportTo('tr.ui.b', function() {
+ const elidedTitleCache = new tr.ui.b.ElidedTitleCache();
+ const ColorScheme = tr.b.ColorScheme;
+ const colorsAsStrings = ColorScheme.colorsAsStrings;
+
+ const EventPresenter = tr.ui.b.EventPresenter;
+ const blackColorId = ColorScheme.getColorIdForReservedName('black');
+
+ /**
+ * This value is used to allow for consistent style UI elements.
+ * Thread time visualisation uses a smaller rectangle that has this height.
+ * @const
+ */
+ const THIN_SLICE_HEIGHT = 4;
+
+ /**
+ * This value is used to for performance considerations when drawing large
+ * zoomed out traces that feature cpu time in the slices. If the waiting
+ * width is less than the threshold, we only draw the rectangle as a solid.
+ * @const
+ */
+ const SLICE_WAITING_WIDTH_DRAW_THRESHOLD = 3;
+
+ /**
+ * If the slice has mostly been waiting to be scheduled on the cpu, the
+ * wall clock will be far greater than the cpu clock. Draw the slice
+ * only as an idle slice, if the active width is not thicker than the
+ * threshold.
+ * @const
+ */
+ const SLICE_ACTIVE_WIDTH_DRAW_THRESHOLD = 1;
+
+ /**
+ * Should we elide text on trace labels?
+ * Without eliding, text that is too wide isn't drawn at all.
+ * Disable if you feel this causes a performance problem.
+ * This is a default value that can be overridden in tracks for testing.
+ * @const
+ */
+ const SHOULD_ELIDE_TEXT = true;
+
+ /**
+ * Draw the define line into |ctx|.
+ *
+ * @param {Context} ctx The context to draw into.
+ * @param {float} x1 The start x position of the line.
+ * @param {float} y1 The start y position of the line.
+ * @param {float} x2 The end x position of the line.
+ * @param {float} y2 The end y position of the line.
+ */
+ function drawLine(ctx, x1, y1, x2, y2) {
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ }
+
+ /**
+ * Draw the defined triangle into |ctx|.
+ *
+ * @param {Context} ctx The context to draw into.
+ * @param {float} x1 The first corner x.
+ * @param {float} y1 The first corner y.
+ * @param {float} x2 The second corner x.
+ * @param {float} y2 The second corner y.
+ * @param {float} x3 The third corner x.
+ * @param {float} y3 The third corner y.
+ */
+ function drawTriangle(ctx, x1, y1, x2, y2, x3, y3) {
+ ctx.beginPath();
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.lineTo(x3, y3);
+ ctx.closePath();
+ }
+
+ /**
+ * Draw an arrow into |ctx|.
+ *
+ * @param {Context} ctx The context to draw into.
+ * @param {float} x1 The shaft x.
+ * @param {float} y1 The shaft y.
+ * @param {float} x2 The head x.
+ * @param {float} y2 The head y.
+ * @param {float} arrowLength The length of the head.
+ * @param {float} arrowWidth The width of the head.
+ */
+ function drawArrow(ctx, x1, y1, x2, y2, arrowLength, arrowWidth) {
+ const dx = x2 - x1;
+ const dy = y2 - y1;
+ const len = Math.sqrt(dx * dx + dy * dy);
+ const perc = (len - arrowLength) / len;
+ const bx = x1 + perc * dx;
+ const by = y1 + perc * dy;
+ const ux = dx / len;
+ const uy = dy / len;
+ const ax = uy * arrowWidth;
+ const ay = -ux * arrowWidth;
+
+ ctx.beginPath();
+ drawLine(ctx, x1, y1, x2, y2);
+ ctx.stroke();
+
+ drawTriangle(ctx,
+ bx + ax, by + ay,
+ x2, y2,
+ bx - ax, by - ay);
+ ctx.fill();
+ }
+
+ /**
+ * Draw the provided slices to the screen.
+ *
+ * Each of the elements in |slices| must provide the follow methods:
+ * * start
+ * * duration
+ * * colorId
+ * * selected
+ *
+ * @param {Context} ctx The canvas context.
+ * @param {TimelineDrawTransform} dt The draw transform.
+ * @param {float} viewLWorld The left most point of the world viewport.
+ * @param {float} viewRWorld The right most point of the world viewport.
+ * @param {float} viewHeight The height of the viewport.
+ * @param {Array} slices The slices to draw.
+ * @param {bool} async Whether the slices are drawn with async style.
+ */
+ function drawSlices(ctx, dt, viewLWorld, viewRWorld, viewHeight, slices,
+ async) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const height = viewHeight * pixelRatio;
+ const viewL = dt.xWorldToView(viewLWorld);
+ const viewR = dt.xWorldToView(viewRWorld);
+
+ let darkRectHeight = THIN_SLICE_HEIGHT * pixelRatio;
+
+ // Not enough space for both colors, use light color only.
+ if (height < darkRectHeight) {
+ darkRectHeight = 0;
+ }
+
+ const lightRectHeight = height - darkRectHeight;
+
+ ctx.save();
+ const rect = new tr.ui.b.FastRectRenderer(
+ ctx, viewL, viewR, 2, 2, colorsAsStrings);
+ rect.setYandH(0, height);
+
+ const lowSlice = tr.b.findLowIndexInSortedArray(
+ slices,
+ function(slice) { return slice.start + slice.duration; },
+ viewLWorld);
+
+ let hadTopLevel = false;
+
+ for (let i = lowSlice; i < slices.length; ++i) {
+ const slice = slices[i];
+ const x = slice.start;
+ if (x > viewRWorld) break;
+
+ const xView = dt.xWorldToView(x);
+ let wView = 1;
+ if (slice.duration > 0) {
+ const w = Math.max(slice.duration, 0.000001);
+ wView = Math.max(dt.xWorldVectorToView(w), 1);
+ }
+
+ const colorId = EventPresenter.getSliceColorId(slice);
+ const alpha = EventPresenter.getSliceAlpha(slice, async);
+ const lightAlpha = alpha * 0.70;
+
+ if (async && slice.isTopLevel) {
+ rect.setYandH(3, height - 3);
+ hadTopLevel = true;
+ } else {
+ rect.setYandH(0, height);
+ }
+
+ // If cpuDuration is available, draw rectangles proportional to the
+ // amount of cpu time taken.
+ if (!slice.cpuDuration) {
+ // No cpuDuration available, draw using only one alpha.
+ rect.fillRect(xView, wView, colorId, alpha);
+ continue;
+ }
+
+ let activeWidth = wView * (slice.cpuDuration / slice.duration);
+ let waitingWidth = wView - activeWidth;
+
+ // Check if we have enough screen space to draw the whole slice, with
+ // both color tones.
+ //
+ // Truncate the activeWidth to 0 if it is less than 'threshold' pixels.
+ if (activeWidth < SLICE_ACTIVE_WIDTH_DRAW_THRESHOLD) {
+ activeWidth = 0;
+ waitingWidth = wView;
+ }
+
+ // Truncate the waitingWidth to 0 if it is less than 'threshold' pixels.
+ if (waitingWidth < SLICE_WAITING_WIDTH_DRAW_THRESHOLD) {
+ activeWidth = wView;
+ waitingWidth = 0;
+ }
+
+ // We now draw the two rectangles making up the event slice.
+ // NOTE: The if statements are necessary for performance considerations.
+ // We do not want to force draws, if the width of the rectangle is 0.
+ //
+ // First draw the solid color, representing the 'active' part.
+ if (activeWidth > 0) {
+ rect.fillRect(xView, activeWidth, colorId, alpha);
+ }
+
+ // Next draw the two toned 'idle' part.
+ // NOTE: We subtract 1 from the left-hand edge and draw one extra pixel to
+ // prevent drawing artifacts. Without this, the two parts of the slice
+ // ('active' and 'idle') may appear split apart.
+ if (waitingWidth > 0) {
+ // First draw the light toned top part.
+ rect.setYandH(0, lightRectHeight);
+ rect.fillRect(xView + activeWidth - 1,
+ waitingWidth + 1, colorId, lightAlpha);
+ // Then the solid bottom half.
+ rect.setYandH(lightRectHeight, darkRectHeight);
+ rect.fillRect(xView + activeWidth - 1,
+ waitingWidth + 1, colorId, alpha);
+ // Reset for the next slice.
+ rect.setYandH(0, height);
+ }
+ }
+ rect.flush();
+
+ if (async && hadTopLevel) {
+ // Draw a top border over async slices in order to visually separate
+ // them from events above it.
+ // See https://github.com/google/trace-viewer/issues/725.
+ rect.setYandH(2, 1);
+ for (let i = lowSlice; i < slices.length; ++i) {
+ const slice = slices[i];
+ const x = slice.start;
+ if (x > viewRWorld) break;
+
+ if (!slice.isTopLevel) continue;
+
+ const xView = dt.xWorldToView(x);
+ let wView = 1;
+ if (slice.duration > 0) {
+ const w = Math.max(slice.duration, 0.000001);
+ wView = Math.max(dt.xWorldVectorToView(w), 1);
+ }
+
+ rect.fillRect(xView, wView, blackColorId, 0.7);
+ }
+ rect.flush();
+ }
+
+ ctx.restore();
+ }
+
+ /**
+ * Draw the provided instant slices as lines to the screen.
+ *
+ * Each of the elements in |slices| must provide the follow methods:
+ * * start
+ * * duration with value of 0.
+ * * colorId
+ * * selected
+ *
+ * @param {Context} ctx The canvas context.
+ * @param {TimelineDrawTransform} dt The draw transform.
+ * @param {float} viewLWorld The left most point of the world viewport.
+ * @param {float} viewRWorld The right most point of the world viewport.
+ * @param {float} viewHeight The height of the viewport.
+ * @param {Array} slices The slices to draw.
+ * @param {Numer} lineWidthInPixels The width of the lines.
+ */
+ function drawInstantSlicesAsLines(
+ ctx, dt, viewLWorld, viewRWorld, viewHeight, slices, lineWidthInPixels) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const height = viewHeight * pixelRatio;
+
+ ctx.save();
+ ctx.lineWidth = lineWidthInPixels * pixelRatio;
+
+ const lowSlice = tr.b.findLowIndexInSortedArray(
+ slices,
+ function(slice) { return slice.start; },
+ viewLWorld);
+
+ for (let i = lowSlice; i < slices.length; ++i) {
+ const slice = slices[i];
+ const x = slice.start;
+ if (x > viewRWorld) break;
+
+ ctx.strokeStyle = EventPresenter.getInstantSliceColor(slice);
+
+ const xView = dt.xWorldToView(x);
+
+ ctx.beginPath();
+ ctx.moveTo(xView, 0);
+ ctx.lineTo(xView, height);
+ ctx.stroke();
+ }
+ ctx.restore();
+ }
+
+ /**
+ * Draws the labels for the given slices.
+ *
+ * The |slices| array must contain objects with the following API:
+ * * start
+ * * duration
+ * * title
+ * * didNotFinish (optional)
+ *
+ * @param {Context} ctx The graphics context.
+ * @param {TimelineDrawTransform} dt The draw transform.
+ * @param {float} viewLWorld The left most point of the world viewport.
+ * @param {float} viewRWorld The right most point of the world viewport.
+ * @param {Array} slices The slices to label.
+ * @param {bool} async Whether the slice labels are drawn with async style.
+ * @param {float} fontSize The font size.
+ * @param {float} yOffset The font offset.
+ */
+ function drawLabels(ctx, dt, viewLWorld, viewRWorld, slices, async,
+ fontSize, yOffset) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const pixWidth = dt.xViewVectorToWorld(1);
+
+ ctx.save();
+
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ ctx.font = (fontSize * pixelRatio) + 'px sans-serif';
+
+ if (async) {
+ ctx.font = 'italic ' + ctx.font;
+ }
+
+ const cY = yOffset * pixelRatio;
+
+ const lowSlice = tr.b.findLowIndexInSortedArray(
+ slices,
+ function(slice) { return slice.start + slice.duration; },
+ viewLWorld);
+
+ // Don't render text until it is 20px wide
+ const quickDiscardThreshold = pixWidth * 20;
+ for (let i = lowSlice; i < slices.length; ++i) {
+ const slice = slices[i];
+ if (slice.start > viewRWorld) break;
+
+ if (slice.duration <= quickDiscardThreshold) continue;
+
+ // Clip slice boundaries to viewport.
+ const xLeftClipped = Math.max(slice.start, viewLWorld);
+ const xRightClipped = Math.min(slice.start + slice.duration, viewRWorld);
+ const visibleWidth = xRightClipped - xLeftClipped;
+
+ const title = slice.title +
+ (slice.didNotFinish ? ' (Did Not Finish)' : '');
+
+ let drawnTitle = title;
+ let drawnWidth = elidedTitleCache.labelWidth(ctx, drawnTitle);
+ const fullLabelWidth = elidedTitleCache.labelWidthWorld(
+ ctx, drawnTitle, pixWidth);
+ if (SHOULD_ELIDE_TEXT && fullLabelWidth > visibleWidth) {
+ const elidedValues = elidedTitleCache.get(
+ ctx, pixWidth,
+ drawnTitle, drawnWidth,
+ visibleWidth);
+ drawnTitle = elidedValues.string;
+ drawnWidth = elidedValues.width;
+ }
+
+ if (drawnWidth * pixWidth < visibleWidth) {
+ ctx.fillStyle = EventPresenter.getTextColor(slice);
+ const cX = dt.xWorldToView((xLeftClipped + xRightClipped) / 2);
+ ctx.fillText(drawnTitle, cX, cY, drawnWidth);
+ }
+ }
+ ctx.restore();
+ }
+
+ return {
+ drawSlices,
+ drawInstantSlicesAsLines,
+ drawLabels,
+
+ drawLine,
+ drawTriangle,
+ drawArrow,
+
+ elidedTitleCache_: elidedTitleCache,
+
+ THIN_SLICE_HEIGHT,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html
new file mode 100644
index 00000000000..0c37e9910b7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<dom-module id='tr-ui-b-dropdown'>
+ <template>
+ <style>
+ button {
+ @apply --dropdown-button;
+ }
+ button.open {
+ @apply --dropdown-button-open;
+ }
+ dialog {
+ position: absolute;
+ margin: 0;
+ padding: 1em;
+ border: 1px solid darkgrey;
+ @apply --dropdown-dialog;
+ }
+ </style>
+
+ <button id="button" on-tap="open">[[label]]</button>
+
+ <dialog id="dialog" on-tap="onDialogTap_" on-cancel="close">
+ <slot></slot>
+ </dialog>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.ui.b', function() {
+ Polymer({
+ is: 'tr-ui-b-dropdown',
+
+ properties: {
+ label: {
+ type: String,
+ value: '',
+ },
+ },
+
+ open() {
+ if (this.isOpen) return;
+
+ Polymer.dom(this.$.button).classList.add('open');
+ const buttonRect = this.$.button.getBoundingClientRect();
+ this.$.dialog.style.top = buttonRect.bottom - 1 + 'px';
+ this.$.dialog.style.left = buttonRect.left + 'px';
+ this.$.dialog.showModal();
+
+ const dialogRect = this.$.dialog.getBoundingClientRect();
+ if (dialogRect.right > window.innerWidth) {
+ // If the dialog's right edge falls past the right edge of the window,
+ // then move the dialog to the left so that its right edge lines up with
+ // the button's right edge, but not so far left that its left edge falls
+ // past the left edge of the window.
+ this.$.dialog.style.left = Math.max(0, buttonRect.right -
+ dialogRect.width) + 'px';
+ }
+ },
+
+ onDialogTap_(event) {
+ // Clicking on elements inside the dialog should never close it.
+ if (event.detail.sourceEvent.srcElement !== this.$.dialog) return;
+
+ // Close the dialog when the user clicks on the backdrop outside the
+ // dialog, which sends click events to the dialog even though the
+ // coordinates are outside the dialog.
+ const dialogRect = this.$.dialog.getBoundingClientRect();
+ let inside = true;
+ inside &= event.detail.x >= dialogRect.left;
+ inside &= event.detail.x < dialogRect.right;
+ inside &= event.detail.y >= dialogRect.top;
+ inside &= event.detail.y < dialogRect.bottom;
+ if (inside) return;
+
+ event.preventDefault();
+ this.close();
+ },
+
+ close() {
+ if (!this.isOpen) return;
+ this.$.dialog.close();
+ Polymer.dom(this.$.button).classList.remove('open');
+ this.$.button.focus();
+ },
+
+ get isOpen() {
+ return this.$.button.classList.contains('open');
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html
new file mode 100644
index 00000000000..bdc118a4d09
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/dropdown_test.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/dropdown.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function dispatchClick(elem, x, y) {
+ const clickEvent = document.createEvent('MouseEvents');
+ const bubbles = true;
+ const cancelable = false;
+ const button = 0;
+ const ctrlKey = false;
+ const altKey = false;
+ const shiftKey = false;
+ const metaKey = false;
+ clickEvent.initMouseEvent(
+ 'click', bubbles, cancelable, document.defaultView, button, x, y, x,
+ y, ctrlKey, altKey, shiftKey, metaKey, button, elem);
+ elem.dispatchEvent(clickEvent);
+ }
+
+ test('basic', function() {
+ const dd = document.createElement('tr-ui-b-dropdown');
+ dd.style.marginLeft = '50px';
+ dd.style.width = '50px';
+ dd.label = 'Settings';
+
+ const textDiv = tr.ui.b.createDiv({textContent: 'text'});
+ Polymer.dom(dd).appendChild(textDiv);
+ const target = {};
+ const checkbox = tr.ui.b.createCheckBox(
+ target, 'enabled', undefined, true, 'checkbox');
+ const actualCheckbox = checkbox.querySelector('input');
+ Polymer.dom(dd).appendChild(checkbox);
+
+ const container = tr.ui.b.createDiv();
+ container.style.height = '100px';
+ Polymer.dom(container).appendChild(dd);
+ Polymer.dom(container).appendChild(
+ tr.ui.b.createDiv({textContent: 'some text'}));
+ this.addHTMLOutput(container);
+
+ dd.open();
+ assert.isTrue(dd.isOpen);
+
+ dd.close();
+ assert.isFalse(dd.isOpen);
+
+ dd.open();
+ assert.isTrue(dd.isOpen);
+
+ // Dispatching a click event at contents of the dropdown should never close
+ // it, even if it is outside of the dropdown, which can happen if the user
+ // presses the spacebar while the checkbox is focused.
+ const actualCheckboxRect = actualCheckbox.getBoundingClientRect();
+ dispatchClick(
+ actualCheckbox, actualCheckboxRect.left, actualCheckboxRect.top);
+ assert.isTrue(dd.isOpen);
+ dispatchClick(actualCheckbox, 0, 0);
+ assert.isTrue(dd.isOpen);
+
+ const textDivRect = textDiv.getBoundingClientRect();
+ dispatchClick(textDiv, textDivRect.left, textDivRect.top);
+ assert.isTrue(dd.isOpen);
+ dispatchClick(textDiv, 0, 0);
+ assert.isTrue(dd.isOpen);
+
+ // Clicking outside the dropdown should close it.
+ dispatchClick(dd.$.dialog, 0, 0);
+ assert.isFalse(dd.isOpen);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html b/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html
new file mode 100644
index 00000000000..85e6cf681f6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/elided_cache.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides a caching layer for elided text values.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Cache for elided strings.
+ * Moved from the ElidedTitleCache protoype to a "global" for speed
+ * (variable reference is 100x faster).
+ * key: String we wish to elide.
+ * value: Another dict whose key is width
+ * and value is an ElidedStringWidthPair.
+ */
+ const elidedTitleCacheDict = new Map();
+ const elidedTitleCache = new ElidedTitleCache();
+
+ /**
+ * A cache for elided strings.
+ * @constructor
+ */
+ function ElidedTitleCache() {
+ // TODO(jrg): possibly obsoleted with the elided string cache.
+ // Consider removing.
+ this.textWidthMap = new Map();
+ }
+
+ ElidedTitleCache.prototype = {
+ /**
+ * Return elided text.
+ *
+ * @param {ctx} Context The graphics context.
+ * @param {pixWidth} Pixel width.
+ * @param {title} Original title text.
+ * @param {width} Drawn width in world coords.
+ * @param {sliceDuration} Where the title must fit (in world coords).
+ * @return {ElidedStringWidthPair} Elided string and width.
+ */
+ get(ctx, pixWidth, title, width, sliceDuration) {
+ let elidedDict = elidedTitleCacheDict.get(title);
+ if (!elidedDict) {
+ elidedDict = new Map();
+ elidedTitleCacheDict.set(title, elidedDict);
+ }
+
+ let elidedDictForPixWidth = elidedDict.get(pixWidth);
+ if (!elidedDictForPixWidth) {
+ elidedDict.set(pixWidth, new Map());
+ elidedDictForPixWidth = elidedDict.get(pixWidth);
+ }
+
+ let stringWidthPair = elidedDictForPixWidth.get(sliceDuration);
+ if (stringWidthPair === undefined) {
+ let newtitle = title;
+ let elided = false;
+ while (this.labelWidthWorld(ctx, newtitle, pixWidth) > sliceDuration) {
+ if (newtitle.length * 0.75 < 1) break;
+ newtitle = newtitle.substring(0, newtitle.length * 0.75);
+ elided = true;
+ }
+
+ if (elided && newtitle.length > 3) {
+ newtitle = newtitle.substring(0, newtitle.length - 3) + '...';
+ }
+
+ stringWidthPair = new ElidedStringWidthPair(
+ newtitle, this.labelWidth(ctx, newtitle));
+ elidedDictForPixWidth.set(sliceDuration, stringWidthPair);
+ }
+ return stringWidthPair;
+ },
+
+ quickMeasureText_(ctx, text) {
+ let w = this.textWidthMap.get(text);
+ if (!w) {
+ w = ctx.measureText(text).width;
+ this.textWidthMap.set(text, w);
+ }
+ return w;
+ },
+
+ labelWidth(ctx, title) {
+ return this.quickMeasureText_(ctx, title) + 2;
+ },
+
+ labelWidthWorld(ctx, title, pixWidth) {
+ return this.labelWidth(ctx, title) * pixWidth;
+ }
+ };
+
+ /**
+ * A pair representing an elided string and world-coordinate width
+ * to draw it.
+ * @constructor
+ */
+ function ElidedStringWidthPair(string, width) {
+ this.string = string;
+ this.width = width;
+ }
+
+ return {
+ ElidedTitleCache,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html
new file mode 100644
index 00000000000..977561e2787
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides color scheme related functions.
+ */
+tr.exportTo('tr.ui.b', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ const colors = ColorScheme.colors;
+ const colorsAsStrings = ColorScheme.colorsAsStrings;
+
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * Provides methods to get view values for events.
+ */
+ const EventPresenter = {
+ getSelectableItemColorAsString(item) {
+ const offset = this.getColorIdOffset_(item);
+ const colorId = ColorScheme.getVariantColorId(item.colorId, offset);
+ return colorsAsStrings[colorId];
+ },
+
+ getColorIdOffset_(event) {
+ return event.selectionState;
+ },
+
+ getTextColor(event) {
+ if (event.selectionState === SelectionState.DIMMED) {
+ return 'rgb(60,60,60)';
+ }
+ return 'rgb(0,0,0)';
+ },
+
+ getSliceColorId(slice) {
+ const offset = this.getColorIdOffset_(slice);
+ return ColorScheme.getVariantColorId(slice.colorId, offset);
+ },
+
+ getSliceAlpha(slice, async) {
+ let alpha = 1;
+ if (async) {
+ alpha *= 0.3;
+ }
+ return alpha;
+ },
+
+ getInstantSliceColor(instant) {
+ const offset = this.getColorIdOffset_(instant);
+ const colorId = ColorScheme.getVariantColorId(instant.colorId, offset);
+ return colors[colorId].toStringWithAlphaOverride(1.0);
+ },
+
+ getObjectInstanceColor(instance) {
+ const offset = this.getColorIdOffset_(instance);
+ const colorId = ColorScheme.getVariantColorId(instance.colorId, offset);
+ return colors[colorId].toStringWithAlphaOverride(0.25);
+ },
+
+ getObjectSnapshotColor(snapshot) {
+ const offset = this.getColorIdOffset_(snapshot);
+ let colorId = snapshot.objectInstance.colorId;
+ colorId = ColorScheme.getVariantColorId(colorId, offset);
+ return colors[colorId];
+ },
+
+ getCounterSeriesColor(colorId, selectionState,
+ opt_alphaMultiplier) {
+ const event = {selectionState};
+ const offset = this.getColorIdOffset_(event);
+ const c = colors[ColorScheme.getVariantColorId(colorId, offset)];
+ return c.toStringWithAlphaOverride(
+ opt_alphaMultiplier !== undefined ? opt_alphaMultiplier : 1.0);
+ },
+
+ getBarSnapshotColor(snapshot, offset) {
+ const snapshotOffset = this.getColorIdOffset_(snapshot);
+ let colorId = snapshot.objectInstance.colorId;
+ colorId = ColorScheme.getAnotherColorId(colorId, offset);
+ colorId = ColorScheme.getVariantColorId(colorId, snapshotOffset);
+ return colors[colorId].toStringWithAlphaOverride(1.0);
+ }
+ };
+
+ return {
+ EventPresenter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html
new file mode 100644
index 00000000000..f9531aa9cdf
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/event_presenter_test.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ function mockEvent(colorId, selectionState) {
+ return { colorId, selectionState };
+ }
+
+ function mockSnapshot(colorId, selectionState) {
+ return { objectInstance: { colorId }, selectionState };
+ }
+
+ function isColor(color) {
+ return color.toString().startsWith('rgb');
+ }
+
+ test('instantSliceColor', function() {
+ const color = EventPresenter.getInstantSliceColor(mockEvent(1, 0));
+ const variant = EventPresenter.getInstantSliceColor(mockEvent(1, 1));
+ assert.isTrue(isColor(color));
+ assert.notStrictEqual(color, variant);
+ });
+
+ test('objectInstanceColor', function() {
+ const color = EventPresenter.getObjectInstanceColor(mockEvent(2, 0));
+ const variant = EventPresenter.getInstantSliceColor(mockEvent(2, 2));
+ assert.isTrue(isColor(color));
+ assert.notStrictEqual(color, variant);
+ });
+
+ test('objectSnapshotColor', function() {
+ const color = EventPresenter.getObjectSnapshotColor(mockSnapshot(3, 0));
+ const variant = EventPresenter.getObjectSnapshotColor(mockSnapshot(3, 3));
+ assert.isTrue(isColor(color));
+ assert.notStrictEqual(color, variant);
+ });
+
+ test('counterSeriesColor', function() {
+ const color = EventPresenter.getCounterSeriesColor(1, 0);
+ const variant = EventPresenter.getCounterSeriesColor(1, 1);
+ const transparent = EventPresenter.getCounterSeriesColor(1, 0, 0.0);
+ assert.isTrue(isColor(color));
+ assert.isTrue(isColor(transparent));
+ assert.notStrictEqual(color, variant);
+ assert.notStrictEqual(variant, transparent);
+ assert.notStrictEqual(transparent, color);
+ });
+
+ test('barSnapshotColor', function() {
+ const color = EventPresenter.getBarSnapshotColor(mockSnapshot(1, 2), 1000);
+ assert.isTrue(isColor(color));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html b/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html
new file mode 100644
index 00000000000..750140bc8f9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/fast_rect_renderer.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides a mechanism for drawing massive numbers of
+ * colored rectangles into a canvas in an efficient manner, provided
+ * they are drawn left to right with fixed y and height throughout.
+ *
+ * The basic idea used here is to fuse subpixel rectangles together so that
+ * we never issue a canvas fillRect for them. It turns out Javascript can
+ * do this quite efficiently, compared to asking Canvas2D to do the same.
+ *
+ * Rather than expending compute cycles trying to figure out an average
+ * color for fused rectangles from css strings, you instead draw using
+ * palettized colors. The fused rect color is chosen from the rectangle with
+ * the higher alpha value, if equal the max palette index encountered.
+ *
+ * Make sure to flush the trackRenderer before finishing drawing in order
+ * to commit any queued drawing operations.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Creates a fast rect renderer with a specific set of culling rules
+ * and color palette.
+ *
+ * Rectangles that are drawn will be clipped horizontally to the range
+ * [xMin, xMax]; this is done because CanvasRenderingContext2D does not draw
+ * rectangles with coordinates of very large magnitude correctly.
+ *
+ * @param {GraphicsContext2D} ctx Canvas2D drawing context.
+ * @param {number} xMin Left border of the viewport (pre-transformation).
+ * @param {number} xMax Right border of the viewport (pre-transformation).
+ * @param {number} minRectSize Only rectangles with width < minRectSize are
+ * considered for merging.
+ * @param {number} maxMergeDist Only rectangles that are at most this far
+ apart are considered for merging.
+ * @param {Array} palette The color palette for drawing. Palette slots
+ * should map to valid Canvas fillStyle strings.
+ *
+ * @constructor
+ */
+ function FastRectRenderer(
+ ctx, xMin, xMax, minRectSize, maxMergeDist, palette) {
+ this.ctx_ = ctx;
+ this.xMin_ = xMin;
+ this.xMax_ = xMax;
+ this.minRectSize_ = minRectSize;
+ this.maxMergeDist_ = maxMergeDist;
+ this.palette_ = palette;
+ }
+
+ FastRectRenderer.prototype = {
+ y_: 0,
+ h_: 0,
+ merging_: false,
+ mergeStartX_: 0,
+ mergeCurRight_: 0,
+ mergedColorId_: 0,
+ mergedAlpha_: 0,
+
+ /**
+ * Changes the y position and height for subsequent fillRect
+ * calls. x and width are specified on the fillRect calls.
+ */
+ setYandH(y, h) {
+ if (this.y_ === y &&
+ this.h_ === h) {
+ return;
+ }
+ this.flush();
+ this.y_ = y;
+ this.h_ = h;
+ },
+
+ /**
+ * Fills rectangle at the specified location, if visible. If the
+ * rectangle is subpixel, it will be merged with adjacent rectangles.
+ * The drawing operation may not take effect until flush is called.
+ * @param {number} colorId The color of this rectangle, as an index
+ * in the renderer's color palette.
+ * @param {number} alpha The opacity of the rectangle as 0.0-1.0 number.
+ */
+ fillRect(x, w, colorId, alpha) {
+ const r = x + w;
+ if (w < this.minRectSize_) {
+ if (r - this.mergeStartX_ > this.maxMergeDist_) {
+ this.flush();
+ }
+ if (!this.merging_) {
+ this.merging_ = true;
+ this.mergeStartX_ = x;
+ this.mergeCurRight_ = r;
+ this.mergedColorId_ = colorId;
+ this.mergedAlpha_ = alpha;
+ } else {
+ this.mergeCurRight_ = r;
+
+ if (this.mergedAlpha_ < alpha ||
+ (this.mergedAlpha_ === alpha && this.mergedColorId_ < colorId)) {
+ this.mergedAlpha_ = alpha;
+ this.mergedColorId_ = colorId;
+ }
+ }
+ } else {
+ if (this.merging_) {
+ this.flush();
+ }
+ this.ctx_.fillStyle = this.palette_[colorId];
+ this.ctx_.globalAlpha = alpha;
+ const xLeft = Math.max(x, this.xMin_);
+ const xRight = Math.min(r, this.xMax_);
+ if (xLeft < xRight) {
+ this.ctx_.fillRect(xLeft, this.y_, xRight - xLeft, this.h_);
+ }
+ }
+ },
+
+ /**
+ * Commits any pending fillRect operations to the underlying graphics
+ * context.
+ */
+ flush() {
+ if (this.merging_) {
+ this.ctx_.fillStyle = this.palette_[this.mergedColorId_];
+ this.ctx_.globalAlpha = this.mergedAlpha_;
+ const xLeft = Math.max(this.mergeStartX_, this.xMin_);
+ const xRight = Math.min(this.mergeCurRight_, this.xMax_);
+ if (xLeft < xRight) {
+ this.ctx_.fillRect(xLeft, this.y_, xRight - xLeft, this.h_);
+ }
+ this.merging_ = false;
+ }
+ }
+ };
+
+ return {
+ FastRectRenderer,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html b/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html
new file mode 100644
index 00000000000..0182602631d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/favicons.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.ui.b', function() {
+ const FaviconsByHue = {
+ blue: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAlrklEQVR4Ae2dCXwdVb3H5265yc3SpEk3ukEXCqVUBLT4Wm19oFKtaN0fKijy9CMguPBarIJsIiA8qsjTh7SllAoFeVBaEARkLV1ooXtL0yRdkqZp9u3uy/v/5uY/OZm75y659+acdnLOnP385zv/+58zZ2YMinTplIAhzsoDceaT2RKUQLwHIMFqh0V2ll0kn4XA6byv9/Vw834kX19e7keRQCzhRyk6bJJYRvD1YTXuhRdeqDj77LPPtNls400mU7HRaCzFFggEVJ/iSqhsicFgKIXUKL6bvB6fz9fj9/u7Kb4bPjaK67Xb7Q0HDhw49IUvfKEd2XUb7WpxHIYvXRgJ8AELkzRso1gmKrwkBfjG7373u5Zly5ZNKS8vn2G1Ws80m83YphPI0wnQUemQFp0IzQR9tdfrxXbI5XId6ujo+PCuu+6qXbNmjYfa9NMmngDoBmt+hIe944M53AUhwqwCvXTp0qJrr732opKSkk8XFhZ+imC+gIAryAZB0QnlJuB3OJ3Ot3p6el5/6KGHttxzzz0O6pse+GEP+3AGnKE2EhgG0tAFt99++4WkoT9tsVgW0DaH4guzAeg4+uD0eDxbaXuDNPzrt9xyy3bS8G4qB8BF6OOoKr+yDDfAB0B91VVXFf72t7+9lLT05QUFBZfQoYWtnA+ux+12v0ra/W+/+tWvXlq5cqWTBjUsYR8OgDPU8KGtjR9++OHHx4wZ8+2ioqKv0X4lbfnsWh0Ox9+bmprWzpgxYxsNFBpd1Op5bcbkM+AMtgr11q1bTz/zzDP/gy4Qv02zGtPzmehIY6MZmmq6UF176NChJ+bMmXOkD3QR9khFczY+HwEXwTbV1NTMI229FCYIXSTm43gTho8uUgMwYUir3zN16tR3qAIfbXkJej4dcIxF1dbkm44ePfqZqqqqpTT7MZf2pYsgAZqN2dTS0nLP5MmTX6EsDDrDHqFU7kTnA+Aa2BMmTDBv2bLliyNHjlxCZsgFuXMYhr6nZL7saGtru/eiiy7aUF9f76UeAfKcBz2XAUffVbgJbAuB/Y3KysoldONl5tDjkrs9oBtL+1tbWwH6UwS6/mZSzg0sVwHXTJG9e/deOGXKlOWksS/MOelncYdJo2+vra396axZs7ZTN0XTJYt7Hdq1XANc1dg0DNOqVatGLl68+DZa/3E1XTwCeOn6JLCly6ncU9+mNLnBZRLOYPAHHI5H2l5/8TdHbl3SRjUx6DkztZgrgKOfDLf5xIkT36moqLiLzJG0rAFJAomsKDp1W51S74IZnSIX8DcrXV3LlK/Oe5xqZPsckGc96LkAOPpowrZ79+5ZNK31BzkzQtKI4qxvV0dJTSLJ592kHKu7QfnPxXupFmhzbFkNeTb/tGsae/bs2Va6wr/lrLPO2izhTgLQZIuaaMp1yvTNyvNbb1HomFB1ZtrAUNYqymztGMNt2rhx44T58+evohs1n0r2+AyX8mnT4KIAvZ63lA82f1/55TX1FJ21tnk2As4zJObq6urP0BTgCmlri2TFDmcEcHQDtnlz4w+Uyz+Hm0Rsm2PuPGtcNpkomtZesGBBYXNz8210d+05CXfWsBLaEQNd5I+e8JyyYettCh0zyoBrpawyWbJFg2twv/jiixPnzZu3mhZFzQ2VqIyJRwIZ0+BiZzyeTcqebVcqS350nKKzxmTJBsDRB3WWZN++fXPpps060tpVouxkODEJDAng6GIg0KI0Hv+mcsXnN9FeVsyyDLWJwnCbadXfomnTpm2UcCcGc1blNhiqlNMmblT+9soi6hdmWKC4hlSJDiXgaBsCsNDKvysnTpz4JIWLaJMupyVgKFLGjHtSefrNK2kYFtpwjIeMs6FqWIOb7kr+Yty4cX+m2+0446XLBwkESHuPrPqz8uymX9BwhhTyoQBchZseQiigdcj30grAO+SDCPlAtW4MeLikdMQdyvqt9yp0rCl1SDR5pgFX4V64cGERvdhmRWlp6XU6scjdfJNAcfF1ysqNK5Q5C2F+ZhzyTF4AqHCPGjXKSjdwHqUHfr+ab8cyW8YzZLMo0QTgcj2jfO/S7ynNzS7KxtOI0UqkJC1TGlyFm3pccPDgwfsk3Ck5drlVidX6VWXFxvvAAG0Z0+SZAJzhtjQ2Ni6ld5D8KLeOjOxtyiRgK/6R8uy7S6m+jF14phtwmEBow3L8+PGr6FnJm1MmLFlRbkqgtOxm5am3rgITtIGNtJrJ6QQcHcdPkYUuKL9MsybLKSydlICijKxcrjz+0pdJFKzJ0wZ5ugBnuM27du2aT7ffV9JUIGCXTkqAJEAsjJ2wQlm1fj7tpPWOZzoAB9yo1/zSSy/NoLdJraMwFsdLJyUgSqBQGX/GOuX+FTMoEpCDmZRr8nQBbqIHgovnzp27mtaWlImjkmEpAU0CYGPmR1crF19cTHH4hU854KmuECcMOmo9derUAyNGjLiawtJlWAJZOQ8eTQb27keUyz7xM8qS8jnyVGpwNk0s+/fv/4qEO9oRlWkDJGArvVpZ89JXKC7lMyupApzhNm/YsGH6GWec8eCAAcgdKYFYEhhz2oPK3X+ZTtlSao+nEnDzxWRL0eNmj0q7O9bRlOkhEoA9ft6cR5WPq/Y4IE+J+ZyKSjS7m56jvK+srEzeqQw5epmNyDkbXBRPT8//Kl++6EaKSok9nqwG10yTHTt2fJpWB0q4xYMlw4lLoJhu5z/y3KepYEpMlWQBV7U3mSXFNN99H71YPfEByRJSAqIEwND4yfcpFyzgqcOkGE2mMGtvy2OPPXY9vZjnTLGfMiwlMGgJWCxnKktv/QmVT3pWZbCAM9zmxx9//IzRo0fj0STppARSJ4HykTcqN//3GVRhUqZKMoCrC6no6Zy7yTSxpW5ksiYpAZKA0WhTPj73dxRKakHWYABn7W3Zs2cPvjH5eXlApATSIoGi4i8oK56/tA9ysAr2EnKDARxlzJdddlkJ3dC5N6HWZGYpgUQlMH7SvbRWpYSKsamSUA2JAs7a2/ynP/3pOvrc9eSEWpOZpQQSlYDZPFn54a/xcDoDnpAWTxRw5DfRJ7DL6HUPP060rzK/lMCgJFA+8sfKZd/CqlRc9yXEbCKZWXtbli1b9gN6EX3loDorC0kJJCoBk6lS+ebVP6BiCU8bJgI48ppxU2fs2LHXJNpHmV9KICkJVFZdo3zsY7j5w6ZKXNXFCzhrb/PDDz/8HbK9x8ZVu8wkJZAqCZjNY5Wf3vkdqo4Bj8sWjxdw5DPRt3KKTjvtNNxhkk5KIPMSqBz1E2Xq7ITekBUP4Ky9LevWrfsGae9JmR+ZbFFKgCRgLpik3HL3NygUty0eD+Cq9h4/fnwBbTdIQUsJDKkERo+9QSkr47ubMfmNlQHaG5v56aef/ndaUDVtSAcnG5cSMFumKXc/fDGYpI35jCiXeADH3KOZ7lp+Sy6HjShHmZApCWA57dgJ3wKTtIFNQB7RxQIc6abLL7+cniEesTBiLTJBSiCTEiguWah8/isjqEkAHpXhaIk4M5BuXrp06ZfoOUtcvUonJTD0EjCaipSvff9L1JGYU4bRAEeaCjh9P+fr0jwZ+uMqe9AnAZgpo0Z/nfYY8IgcR0qA9sZmeuCBBybZbLZ/66taelIC2SEBKzF5zTJMWbMdDl5DXDTAVe29aNGib5D2jpQvpEIZISWQEQkYicm5C0QtnjDg6uwJPY72tYx0WDYiJZCoBMorGXDW4iE1hNPMOBMQb1qzZs0MmvueHlJKRmS1BCZYYZoOA2exTFd+dT/eTsuzKSFaPJwkNMDPO++8+fLiMvdA+Z8JJcqPN+9RGnocoZ0PBELjFF2cbjdYIEykvq4wWehd4APb05dBari4gaWCe/p8AT+uFOdT4j7aoJTB7oAGowFurqqqmicBV5QPmgLKX3b7lVbHANmRLLPVVSjnGT6hzFRa44dHHEqIHhQThXC8+YQiqQ66K9rnvakoD1O9DPiAJvSAo8vYjMXFxWZ6U9VFA3IP052fv+5VGntzBW4+SCYl4KtQ/L3tpCBJ0+WpC/hKLgKrvb29DDj41Q4WIvUOcaZHH310lslkGqlPHI77uQd38CgZTBbSVBVKXk+CGYwjS758/ywwS1sIz/oI1uCmmTNnflKaJ7l/OmuQG3migQ9xnvg0W2gaN/2TfYDzoLQDFw5wVYOT/T1XAq7JKacDKuS2csVg1B/unB6W2nkwaiiumEs7rMEBueZEG5zpN9Gt+QKyv+douWQg5yXAkPvtHYO78MxiCZisJXNsVRML7C3HndRN5li1w/WnNPaNDz744Ll0ZpRm8Zhk1wYhAYacjPJBlM7eIgHFUFryxZvPpR6q/Io9DavBJ0yYcJY0T0Qx5U84CDnNrtjb82dQZHqZysefRQPaRltEDc4JRlr7PS1/Ri9HopeAwWRWjLYKQiF/NLnBWgpmocGZY3XYoomCBOybaPXgNKnBVfnk7R8V8qLyvIAcrBoLiqaCXdoYcvXYMeB8KmPfSIBPUVPln7yWQD/kjEEOD7fABsBVfvtGoTIdYoOPHDnSXFhYODmHhyq7noAEgpCPUPyOTiql3QBMoIbsyGo0F04uInYdbW3RTZRbb711AnXZmh3dlr3IhAQYcpooz0RzaWmDTk1r0YLrwS4GwRaJuoMGmXrjOeecI5fHQiLDzKmQF9ILXFXIGYfc8q2jZ4JdBlyFnE9ZHolx1KhR8gJzmMHNw9Ugz8U7nrijWToyZCZFtMEBu7GoqGgiD1j6w08CKuTWUsXv6s65O56GApVdlWM+cnoNbqB3D+JzEdINYwkw5DlnkxvNYJetEdVEETU4Ioy0RLZEzoEPY7r7hh6EvIQ0eQ/FZP/sCpilPgNwKG0VbgyFdzTqCXC8ZFw6KQEAoxgLS3NoPbkR7GosIyxqcBxSgwQcYpCOJWDAOnIrKUbS5AH9M5GcKUt8OiEZcK1HbIMjQiVfAq7JRgb6JADIDQR5tpuuAaMGuGaisAbXIiTgkutwEujX5L2UnJ02uSEIOHdfZVpqcBaH9GNKIKjJQ6yAmOUylYHsa+6cprBZg3MfpA3OkpB+WAkENXmxEnDbs2+e3KABrvU9RINTih56LbMMSAlAAqomL7BRQFOU2SGYgMouOqV1jGHWIrxer50+8iofV8uOQ5a1vVA1OUEecOPtWdlhkxsUH/2saE5lmufBtVifz4erCOmkBGJKIKjJ8V0ETT/GLJPODAG/X8+uOg+O0087BaHB09kJWXd+SSCoyYuUgIceaB/qeXL/AA2uci3a4JB8QGrw/AIwE6NRNbmlcMht8oBftT40ZY2xsw2OsJogAYcopEtUAqomt5Am9w6dJg8ENPNagzysBs/2W7KJCl/mz4wE8OYsg3loNLnKbNAG1+DGqFmDI1LdpA2eGRjytRX19XAEecDr6kMqcyM1BNTrR41ltCxqcAYc6yOlkxIYtASCmhyP9WZ2doVmUXhtL1hWHWtw3lccDkcb1H22L6zROiwDWSmBoCa39mnyDHSRmPV7nG36lliDs1r3t7e31+kzyX0pgcFIQNPkGbrj6be3gV287Z95Vk0U7MCpkdXV1bXyIjMoEPk3eQmokJsKglOIAD1tm6J4Wo7UMsd9PQ+wBse+CvgzzzwjAe+TjvRSIwGGnB4qS02F4WohE8W58zk94CGzKP6XX3652+VyNdN6lFHh6pFxUgKDkQAgDygWxeDzDKZ47DJeV3PvvtfpVQChJgoKs80C+8Xf09NzRJopEIt0qZQAIFfou0GpXoUIVv0uxxHqq8ov+cxzyDShmsFut9elcmCyLikBloAKuZEm71Jsi/vdKrMi4GqTbIMz8cjgw0yK1OB8SKSfagkMgDwVlZMGDzg6oJR9tIFh5lmzwdEMR/pPnjxZiwjppATSJQHVJg/QRaffm3wT9Gvg624GswPgRsXhNLh//fr1u2nRFYCXTkogbRJQbybCXEl2diXgCzh2bthNFQHwAZAz4BgEgEaijz4C29zZ2VkjzRSIRbp0SiAIOT7MgCnExDeyThS/s7uma+vaZqpANFHUbusBZ8i9ra2tWyXgqozknzRLQIMcF56JOiLc19O6lYrB1hmgvVGVCDj2VQ1Ovq+mpmaLBBwikS4TElAhx7vJE55dIWhb6rZQH6G9WYNrXRYBh/ZmDe5buXLlVj85LacMSAmkWQIa5Im0Q4x2bXkUGpzhZo7VWsIBrp4JGzZsaCc7/KDU4olIW+ZNVgL9kMe2x4P2d+dB+86X8NFP1uARAUffWIPDnvHSdOE2CTjEIl0mJRCEPA57nAj3dzXj468qr+SzDa51V9TgiGTAcTZ4yQ7fLAHXZCUDGZSABnlUm5wgba3dDFZpE00Uraf6Bx5YveNM8C5fvnzbJZdc4iwuLqYH7Yavq+ytURq70rRIKIvEGlmZAYswDjZCRBchLUJ0ULeGqYzaQL8AfEj/PA5nz8u/Zw3O2ntAC+EAR0bVnnn33Xe7Gxsb35gyZcqlxhR9mGj/oU7liWfrlPZOd5jRZGfUbK9bmUnPGIYIeEB3B8i1PyUKBHTo+vPFEYrcfpR6orYfR6NZmiUQ8Cs9XU1vbDiyEysI2f5myLVe6wFHAqSlanDyPTt37nz+9NNPTxngv/3DHqW5lV4tkGPO67ErPi+9pgw/mYAGfjyO8zJo+vL6dH2dmc6vb1/fP31/9Pn1+7HK69P15fXt9eUP+LxKR/OB5yk7flrFOfABNehtcCSKgHuvu+66t2n5bGtk7TGgvpg7uQg3BmW22BSTGa8pIwehx+s4L3wxzOXFOM4j+sjHecSwmEcMi3nEsJhHDIt5ENY75IXjMhxWI+P4E6u8Pp3bYV/fHsWDRb/f1Vq3b9XblBzxAhNFowEOte+hlYWO+vr6f6QKcDSaq06F3FQYdeUEow9fDGfLmMU+ieFI/RPziOFU5Y9UT/T4gOJ2tP/D7e7Bmz+hwcNeYKKOcIAjHiaKZqa8+uqr6+l9KYgf9g6QG/sgxwHXbxAQgyCG9fmGal/skxiO1B8xjxhOVf5I9USLV8j+7mjd/Rz1RzRPwGuIiwQ4zBScFaDas3Tp0r0dHR2HpRYPyo8hD+7Jv5mUABj0eeyHjx58Yh+1y4CDVTAb4qIBzpCjEjfNiW+Qd+775dcPeTRdI9NCf+OSlQl98M3RvAFM0sbmCVhNCHAcSah8TYuvXr16PT2MjAql65OAapPjXXzRnP4iCnk5Llw5ToMvhsPlzYU4cQxiOFLfxTxiuC+/3+/xNB9/cz3tito7rHmCIpE0ONJwRrAd7l61alXjkSNHXpBaHKLpd5hZMfELJ3FA9Buy8oESw/p8vC/mEcOcnmu+OAYxHGkcYh4xTPlx38DtaHnhZP3rjZQEDR5xehBF4eIFXDVT1q5d+whp8YhnS7DK4fdXhdyEd/FJl04J+ANef3PDpkeoDTZPkgIcfR2gxe+7776aY8eO/VNq8dDD2A95sjamLE8/eSTggRsuLj2Otn821D5fQ4lxaW8cpWgaHOnQ1pqZQmHXU0899VePxxPWoEeB4ewYchwadhzmw4V4jhPDnJ6oL9YhhuOtRywjhuMtr88n1iGGOZ8YJ4Y5PZKv+H2BthOb/0pl8F5mEfCoFkUswNEHVICLTdVMufPOOw+QFn9TanGIJtTBHjeSucIHCjkQZsfhSOmcL14/2fqSLa/vZ6z6YqXr68M+1p24nK1vHq3++wHaZfMETEaFG2XjARzaWgOcwq4XX3zxYdLiKC9dGAkw5Pqf2czso0OMkRhGXG5u9N5vpb3p/YdpAKy9AR+YjGlJxAs4a3GcPa4lS5bsOnHixGapxUkaEVwQcnqrasYdw80wowMcl/HOJN0gtLfb1bH5yMHHd1FlDDhr75QAjk6yFsdVKyB3bty48UE5owLRRHYa5JgSY8dhniZDPMeJYU5P1BfrEMOR6hHzIBzLcV8j1aePR31cRgxzPjFODPel+xWvv6N5x4OUhCWoYA8MxqW9KV9cJgryAXBocQbcdeONN+6kd4k/J9eoQDyRnQq5se+Fk3yg2UcxDvcdULUmjotcbeQULhtvffr8XC6Sj5a5TORe9KdwXq5PXz5KOn0WUHH2nnyudt/qnVSMtXfMqcH+xuMHHGVYi6sXm2jwpptuWk4PJrfLNSqiSEPDGuShSTImggTUNSdee/uxA2uXUxaGO27bm6uNxwbnvKzF8fOABp2vvfZa89atW/8oLzhZRJF9zVyJnEWmCBKgb14qPZ01f2xv3o03VsE8AXNx295cVSKAo4yoxVXIFy9e/Aw91rZLXnCySCP7Jpo+NNLnPMQvHXAYfjz/UDuXEcNcVowTw5yeal9sQwxHakfMI4bF/HhiyuPq2LV/293PUB6GO2HtjfoHA7g4o4LGnWvWrLnL6XT6pKkCkUZ3gNxAL4HnA4rcCMfrOG+k8rHS420n3nyJthcrPxgK+D2+5oa37qI+qHyRj4vLhLU3xpAo4CjDgOOMUrX4HXfcse/AgQPr6I20SJcuhgQYcvVijS++pN938RpQHD0n1h378Cms99Zrb7CXkBsM4GiAIVenDGnfccMNNzzU0tLSKE2V+OSvmiuYXZFOkwDmvD2e7saa/X99iCLxOBoAF7W3ljfewGABZ1ucpw2d7733XusTTzxxE33+xCNNlfjED3vcqELON2WGr0+WCS03cXtaTmy6qbutppUkyHAnNO+tl/xgAUc9DLmmxWnacAeB/hDdANK3I/cjSCAIebi3d0QokKfRZHcrvZ01D9XtW72DhqjX3mBtUC5ZwGGqaFqcws5LL7109dGjR9+WN4DiPx7DHXLc0HE5Wt7es/m21WCob4PiTOimTjiJJwM46gPg2PiCE2ee/Wc/+9lvyB5vkvY4SSNO12+uxFkgT7LB7vZ6uptq9678DQ3JThsYggkAppgvCg7OJQs4WkUnMH2CMw6dc9ANoJNPPvnkL8ke90p7nCQSpzPS9CFscryHbzhsEEvA7/a2NLzzy46WXSdpV+WH/KQuLFEvu1QAzrY4mypqJ+lVE9u3bdv2Z9jjEnIWd2wfkBsM+W+T9813093K6j/X7l+9nSQjwp3UhaUo5VQAjvoY8gGmysKFC1fSgqxX3G6ckNLFKwEVcu3rY/k5swK729Hb9Mqed29fSXLRmyawCAZ9YSnKOVWAo06GHDTjQgGdti9atOjXdNH5noScpJGAU00VI74+ln+OXv2gOJ0t7x3cduevaXQqJ+TztGDK4IbkUg24aI+rkNNXIrquuOKKG+kBiYNyURZEHr/LR8j99OFXt6v94KH377/R4WjtImkAcBFuMJQS7Q1Jp0NFoHNiBw0Eube2tnbT/PnzFzz3UtMIA76mJV1cEjAYcIhInLgTkuMOZonH3XW8dvdff9zZur+JhtNLGwMO8zal2hviSgfgqBduAOhki7u6u7u3NHWO+yxNidkk5EEhxfM3CHmfSHN0zQq98Fjxunta6w+v+9GphneO0Wj0cKdUc7Nc0wW4qG608AcffNBrMlvfLx0x5XMGo7lAQs6HIbbfLytNnLELZUkOrO2mF2b2nDz64rX1hzccpG7p4YbmBuApd+kCHB3lI8G+2vnOlr0dBYVV+4tKxl1MswWW/gOX8rHlXYUsq+C8ChaeZv8/vOqYvo5hb2l48+d1+9fiNrwId8rmuyMd7HQCLrYJyDXQ20/tOGUxF+6wlU1aYDQWFPGBEwvIcHgJ9MtKE2f4jFkQq9rcnu72xrp//OTIgccx181wY8477XBDBJkGXAO9o2VPm+JzbioZMXWewVRQ2n/g0C3poklgoKyyc57cTxeUXnfHCVrXfU1D7fr9NJ4e2gA4w530OpNoMuK0TAGO9ljlaJB3tVd3u1yNb5ZVzPy40Wyt7L+Y4u5JP5IE+iFnsUbKmfl4zHN7nG3VdXtWXNvU8GYd9QBgZxxujDyTgKM9OAZc9e1dDY6ejoOvl1fNnm0yFY1TaApR/QhoMK/8G0UCGuQGEmUWKHK83jhA89z0gvoPDu1cfn1b864T1H29WZIRzc1iyzTgA+CmTqj7Lkeru6156xsVoy+cQk+fn44DJyHnQxTd1yBXRRk9bzpTsSrQ7/MoLvvJN/a/d9uSno5jLdQew40bOVghmFG4Md5MA4424UJA97rtvub6f71VPupcq9lSNttgNBLj8oZQUFzR/w6UU+ZVOeD2eV2B3u7ax/a9e/PvXI7OTuqxCDcuKDMON6Q2VICjbYacJ/jpHYte/8mjr35gtVUdLCwaPYfmyunDlFKbQ1ixXBByiDRzTl0RGPBiPXd7S8Pbyw68d+/TdAz5YlK8QzkkcEMSQwk42mfI4Wugt53c3uB0nHyttHz6THo4dywOnjRZIK7ojiHPxOw4lg4EYJI4mnfW7V95ff3h9bupd9DarLlhkohTgZk9+/pElS2AA27eVOjt3fW9p4699kr5qFkmc0HZR6TJ0nfEYngDzZUYmQeZrN6ZhEnSeXj1nk2/vr2nsw5vn4LGZrj1i6cG2VLyxYYacIyAz2zW4hro9HPnO3nstZ2FhZX7Cm1j5tCDAEWkyqU2j3HctV+7FJvjWE+CWRKvt6utpeGtX+7f/vv/6zNJGG7McfPFZNpuv8cY/oDkbAAcHRIhF0FXw21N2084HfWv2UonjaHPhEwJaikJ+oAjqdvRINfFD2ZXfSILF5I+Fz2kUP/akT0rlhyv2bCX6mKNDcD1N3CgqIbc4RzPJof+YOoEJx7eioNPl+FDlHSxqdgQnj77h5+oGPeJXxQUlE3Cg7qZ+EmmdnPWYYYjGRec/nMrbnfnsbaT2+6v2f3wZqoPJghDzVOAvNwVDbLCSqbplJTNNsAxKP5hBeR4OBGfSQDkDHpRYWFFyYzzf/Gd4oqpV5JGt+IZxlRqLGorr1zwmdjEmOMZEp/X4erpqFld/f4Djzud7ZghgabGBrDZ1sYsCa/lTqwhKphOly0min6MLCT42KAV2Kbzeb1Ob9Pxf+32utteLSqZOJ4++jRJmi16Efbv95/8rDsi++pzFX3mCM1kvXPkw7X/Vbd31eskc3H6D9pbhBvHJ7mfiv7upjSUjRpcHCD6xyYLa3PW6DBbVM0+4/yffKq88iPXmq0jJuOdf/J2vyjC/nBQk/fviyHRzva6u462N+96qHrng29RHtbUrLUx9cc3bljpsEISq8yKcLYDzkIC5Aw6bHNAzva5CrnZbC6c/pHrLykbefYVZmv5NAk6iy66PwBsV8fhrrYDj1Xv+uOr9GYyBpt9ntcWbe2s1NriiHMFcPSZtTlAhzZn0AE4ww7fOuP86z45ovLcKyzWkecEL0RN0kYnwYguaGP78MJLetl8277O1j2Pffj+n96mPAAZG8BmHxobYPMdSYCdtVqb+qa5XAKcO40+49qBQYc2Z42uAk77qj919tUfqxh1wZXWosrz6cEKslxQbPhOLwZNFKz4I7D9broL2fp+e/OO1TW7H3mPBMNgi75ojgBqvpCkYG64XAQckkW/sYlmCzQ6Ty2KoBeccc53Z5eP/uiXrIWjFpjNRTaD+no0FM1/2DWo6cIRb3D1eh12l7P5jY5TH6yv27cGt9cBsQg1wtDWvIl2dk5obeq75nIVcB4AQ86gs+nCoLNmV7V8YcnY4ikzvr3ANuKMz1mLqi4k0E3q+7nVu6OoIn+cOv9NUyJ4+ACfBKG3t263d9a9XPvh2jecPSdxg4a1M4BmyBlqnvaD1s4ZcyTc0ct1wHlMetBhi7CNziYM+6qmrzrtwtHjJi/6rK1k/OfoiblpAJ1hz0XNzpoai6AANTafu/uwvafh5cajG//ZcmL7KZIJA8xwiz7SoK1ZY+c02DQO1eUL4OJ4grZH0E6HRmetDsAZetE3T5q6eHr5mPPmWQurzjcXls8i0K20VFcx4iWYeA9JFpoyA4CmJatYI0JQu7zOjr0uZ8v7HU073zlW82w1dR7aGPAC5nA+0llj8z2HnDNFaAxhXb4BzoMMUtlvo0Ojs1bXA69qdEqHby4sLLeOm7p4Vln5tAsshRXnFxSMOJseirbgAhXPjAZvmrDYgn7/jRRuPjV+EGLUxbzRBSKWqdJ7RnChGKBPftAt9AMeZ/v7XR2HdzTWPLvX6eyAycFQA2jeGHBOY23NGhuNcEMUzA/HRyo/RhM6ChF0aHbRVhe1O0POceybiovH28ZNW/SR4pJJ55oLiieZzLZJJottPFY2BoHHWnWAT1Wr0owkUn18JJYoHv9xUQiQNd/roJfnNPi89mNed++x3p5jexoPb9zV29uAu4qAlDUx+ww2fI6Dz0CL9nWkzlD23HZ6qef2aKL3HmNl84VBZ83OQEfyOR98lDWOnjB3dFnFOZOttjGTLIWlk81m20RaMlBpUEw2Ay2QoRPARg1SffQXF7F9vtpFaOEgxbSrhuhDAV57gBZ+BBSf3e9ztXq99uMeZ/dRl73pWFf7vqOn6jfBhmYoRe0rwhsuLOZlu5p9tTv5/Gc4Ac7HEWMWN4ZW9AE6Q83Q8z6fHKKvQq+r10DmjrmoZEKx1Ta6yGItK7aYy7AiUvF4u+weV1evy37K4eip7yWzAmBCi4obwwyfta7oI8xAM8TYF/NwWbHevNXWNPYQNxwBF4Uggo4wg8q+CL0IuAg350Ec18H1oi0xjH3RMXiI4zBrVwZcDyxDy1DzPudnn+tjX2x32IQhfOmCEmBZMJDwGXQxLMYBbqSxz5AjDg4+b7wPH9DBMXz6fUCKOEAs+gwv+0gTw9jHBsd+cG+Y/uUDMUyHH3XYLBsGNJIvQq3PgwbEesQGGUDRR1i/Mez6eHEf9WJfOp0EWPi6aLkbQQIsLwYZ2aLFiekRqhwAJkPK8KJMtLhIdcr4PgnwwZECSU4Cejnq91G7Po7BFVvWx+n3xbwyHIcE/h9VLWRYHWXC/QAAAABJRU5ErkJggg==', // @suppress longLineCheck
+
+ green: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAltklEQVR4Ae2dCXQcxZnHR3NoNDp8SD7kU7bxFXCchBhMYoLNmhCcOBBykGw2gYTkPV6AhGXD2sTZJQcJG3jsgw3hscuCsTEsOAQW1sbY+MAHxpYtHzI+5EOy5UMStnWPZkZzab9/j75WTWt6NKO5Z6r82lVdXV1d9e/ffPq6uro7zyBDIhXIi7DyngjLyWJRKhDpCYiy2pwoztrpxSwCb+d1bayFm9f1Yu3+cj2MAgOJH2bXnNnEGiHWppW8d999d/inPvWp6YWFheNMJlOR0WgswdLT06PElFdM+xbn5eWVQDXK76TI7vP57H6/v5PyOxFjobwuh8Nx4dixYye+9rWvtaK4ZqFVNY/TiGUIoQCfsBCbcjaLNVHgJRUQG3/4wx9ali1bNmXYsGEzrFbrdLPZjGUagTyNAB2ZCLXoh3CJoD/p9XqxnOju7j7R1tZ2/LHHHqtbtWqVh47pp0X8AaAZbPmRzvnAJzPXhRBhVoBeunSp7b777ruuuLj4xoKCghsI5s8TcPnpIBT9oNwE/D6Xy7Xdbrd/8Oyzz+5+/PHHndQ2LfA5D3suA85QGwmMPLLQ+b///e/nkIW+0WKxLKBlLuUXpAPQEbTB5fF4KmnZShb+g0ceeaSKLLyb9gPgIvQRVJVdRXIN8CCo77777oI//vGPt5CV/n5+fv5NdGrhK2dDsLvd7k1k3f/n17/+9frly5e7qFM5CXsuAM5QI4a1Nh4/fvza0aNH/4PNZvs2rZfRks2h2el0/u2TTz55dcaMGXuoo7DoolXPajcmmwFnsBWoKysrJ02fPv3v6QLxH2hUY1o2E63XNxqhOUkXqq+eOHHitblz557pBV2EXW/XjM3PRsBFsE21tbXXk7VeCheELhKzsb9Rw0cXqT1wYciqP37FFVd8SBX4aMlK0LPphKMvirWm2FRfX//lESNGLKXRj3m0LoOOAjQas/Py5cuPV1RUbKQiDDrDrrNX5mRnA+Aq2OPHjzfv3r3766WlpUvIDfl85pyG1LeU3Jd9LS0tT1x33XVrzp8/76UWAfKMBz2TAUfbFbgJbAuBfUdZWdkSuvFyZepxydwW0I2lo83NzQD9rwS69mZSxnUsUwFXXZHDhw/PmTJlytNksedknPpp3GCy6FV1dXX/OGvWrCpqpui6pHGr+zct0wBXLDZ1w/TSSy+V3n777b+j+R8/pYtHAC9DrwIO9xHD5c5XDF5fS0ya0MWo3+nwvrBx47nfLLlvKypj0DNmaDFTAEc7GW5zQ0PDD4YPH/4YuSMJmQMSExVpsPPxhjsMHt/FuLWkp8dwqb3dt2zhnD2vUKXsnwPytAc9EwBHG01YDh06NIuGtf5DjoyQGmHC4XMLwmwd/Caft2fnmXr3A3d8Zf9hqgXWHEtaQ57Of9pViz179mwrXeE/MnPmzF0S7sEDGuueJnPevCuusO76sPq6R2bPHm2l+sy0gKG0NZTp2jCG27R27drx8+fPf4lu1NwQ6wnKlf0TZcFF/bwe//Z9uxw/vvfuj89Tftr65ukIOI+QmE+ePPllGgJ8UfraIloDp5MBOFoB37zxQvdPvr5gP24SsW+OsfO0CenkoqhWe8GCBQWXLl36Hd1de1vCnTas9GtIXp5h5LgJ1re3H7z2dwsWjMTUYlwrpZXLki4WXIV73bp1E66//vqVNClK3mLvh1RkGcmy4GJr3B7/zkOVXXfd86PD5yg/bVyWdAAcbVBGSY4cOTKPbtqsJqs9QhRPpqNTIBWAo4U0l+1yw1nXd29duH8nrabFKEuqXRSG20yz/hZPnTp1rYQ7OpjTqTRNUhwxtsK69t3tcxZTuzDCAsOVUiOaSsBxbAhgoZl/d02YMOF1SttokSGDFSCabeVj819/v3LOXdQNCy04xynjLFUHVuGmu5K/HDNmzHN0ux2/eBmyQoEe84gRluc2V13zS+pOSiFPBeAK3PQQQj7NQ36CZgA+Kh9EyAqqgzpBQ4h5w4aZH6URlidwrmljSix5sgFX4F60aJGNXmzzYklJyf1BqsiVrFOgqNh0/5ubJr24aFEp3M+kQ57MCwAF7pEjR1rpBs4KeuD3W1l3NtOkQ6kaRQnXfZfL/+Y3bqz7Ed3f6KZyPIwYbpe4bEuWBVfgphbn19TUPCnhjsu5y6hKCgqM33pr4+QnwQAtSbPkyQCc4bY0NjYupXeQ3JNRZ0Y2Nm4KFBab7tlSdc1SqjBpF56JBhwuEI5hOXfu3N30rOS/xk0tWVFGKjB0mPlfN1bOuRtM0AI2EuomJxJwNBx/iix0QfkNupJ+mtIySAUMpSPyn16z5fPfICnYkicM8kQBznCbq6ur59Pt9+U0FAjYZZAK4J6+aczE/BffWn/1fJIjoXc8EwE44Ea95vXr18+gt0mtpjQmx8sgFVAVIEgKJkzJX/2fq66aQZmAHMzE3ZInCnATPRBcNG/evJU0t2SI2iuZkAoIChiNeUM+O6d45cLbxxVRNv7Cxx3weFeIHwwaar148eJTQ4cO/SmlZUiyAuk4Dh5Ogs5O3wsLPrfnQSoT9zHyeFpw/FhQn+Xo0aPflHCHO6Vym6hASYnpp29v+dw3wQ4tYChuhjdegDPc5jVr1kybPHnyM2IHZFoqMJAC48Zbn/nzi1dNo3Jx9cfjCbh54cKFRfS42Qrpdw90OuV2rQLwx6/9QvGKhQsVfxyQx8WKx6MS/EgUv5vmGTw5ZMgQeadSe/aSvJ5pPrgoj73D91/zr97zEOXFxR+P1YKrrsm+fftupNmBEm7xbMl01AoUlRjvWb1u9o20Y1xclVgBV6w3uSVFNN79JL3LLuoOyR2kAqICYKhisu3JBQvG8tBhTIzGsjNbb8vLL7/8C3oxz3SxoTItFRisAhaLcfqyP435Oe0f86jKYAFnuM2vvPLK5FGjRuHRJBmkAnFToLTM8tCfnpk5mSqMyVWJBXBcWFro6Zw/0Z+Vwrj1TFYkFSAFwNQX5w/5N0rGNCFrMICz9bZ8/PHH+MbkV+UZkQokQoGiQtPX/rb+M7f0Qg5WwV5UYTCAYx/zrbfeWkw3dJ6I6miysFQgSgXGV9ieWHjrqGLajV2VqGqIFnC23ua//OUv99PnriuiOposLBWIUgGLJa9iya8q8HA6Ax6VFY8WcJQ30Sewh9DrHn4WZVtlcanAoBQYXmr62fe+NwGzUnHdFxWz0RRm621ZtmzZT+hF9GWDaq3cSSoQpQImU17ZnfeO+gntFvWwYTSAo6wZN3XKy8vvjbKNsrhUICYFykZa7r1mwUjc/GFXJaL6IgWcrbf5+eef/wH53uUR1S4LSQXipIDZklf+m99N/AFVx4BH5ItHCjjKmehbObaxY8fiDpMMUoGkK0BW/OezZxdH9YasSABn621ZvXr1HWS9Jya9Z/KAUgFSID8/b+KjT02/g5IR++KRAK5Y73HjxuXT8oBUWiqQSgVGlVseoCnZfHdzQH4HKgDrjcX8xhtv/B1NqJqays7JY0sFLPl5U59bVbEQTNLCfOoKEwngGHs0013L78npsLo6yg1JUgAMjhlb8D0wSQvYBOS6YSDAsd30/e9/n54hHrpItxa5QSqQRAWKh5gWffWbY4bSIQF4WIbDbcQvA9vNS5cuvY2es8TVqwxSgZQrYDQabHffU34bNWTAIcNwgGObAjh9P+c70j1J+XmVDehVACyOLs//Dq0y4Loc621g59301FNPTSwsLPyiVFcqkE4K2ArzvvjPv52GIWv2w0P64uEAV6z34sWL76BfjF65dOqzbEsOKQAm5/9diWjFowZcGT2hx9G+nUO6ya5mkAL0WBsDzla8X+tDWWa+uDStWrVqBo19T+u3l8xIawUsplFp3b54NY7mik/703/MxNtpeTSlnxWHk64NKuCf/exn58uLS6086b8+3Pqg4WDNHw0O5yf9Gkuf9+sX6N3twXmaVWwMkUWv+Q7eLVShHk1mv310Kg9Vrt/h/PStQoN/PlVxhBYYa7AbVCwc4PQxzxHXS8ANhkZ7jaGq8W8Gh6ed9MuM4C2ebrD7Jhp6CIJsDr481/UGw4nnqY8MeFB3tYDjF6BY8KKiIjO9qeq6oNI5urL+1L8bOt2XM673PrPf4OjwZDXk/p6e68BqV1cXAx5kxUP54MgzrVixYpbJZCrNuLOagAZnItyQwWQ2GgppXlKeEec8OwON75V+/YErZlHv2A8P6qieBTddeeWVX5LuSZBWGbnCkDs7PQa/PyO7EL7RZI5HTCj+EhXaTwt7IKpfprXgintCBU3kf8+TgIfXNlO2AnJbicVAt7izLoBR2xDLPOoYW/CgP1eiBWf6TXRrPp/877lZp0YOd4ghhyUPNUKRydJYbaa5IyYU5l8+53BRP5hjxYprf9NYNz7zzDOfpl9GSSZ3Wra9vwIMORm9rArUn5Kbfzzt09QphV+xcyEt+Pjx42dK90SUKXvSDDksedYEwnrYyIKZ1J89tOhacN5gpLnfU7Om87Ij/RRgyLPJiFlsZjALC84cK/1GBgdswLqJZg9OzabOcwdl3KcAIC8oNuMtrn2ZGZpCHyxW0xXU/H4Xmgw49xLrRgJ8Sob2VTY7CgVUyLNgnLwXcIXfXgkUpvv54KWlpeaCgoKKKHSSRTNYAQXyIrPB1eXVzOLIrE5ZrcaK0lKbuaXFCbDZYCsuCfcEmcbf/va34ym2cqaMs18BhjyTZ/3TmKB17ncngV1Y8X6AM/XGq65SPsaZ/WdV9jBIAUBuLSSfnPFgIjIoHj2pCFO7xR6oFpy7YRw5cqS8wAw69bmz0gc5cMiwQE0uKrH0G0kRfXDFQbfZbBMyrGuyuXFUQIGc3p/Q7fSRT65O6YjjERJXVX6hCewqHPNRsILAFjyP3j2Iz0XIkMMKBCA3ZdwQosloBLsqyziFogXHBiNNkS3OhrFRdE6GwSsAyPPJkrvJkmeCHVeYNeUBcPbBlc7ziko9AY6XjMsgFVDmkysXnqAjAwIN54NdlWWkRQuOLuRJwCGDDKyA0ZRnsNrM5JOn/zi5yZzHgHPz1VEUZCjkS8BVbWSiVwGGXCEkjVUxGlXA1b85bMHVDAl4Gp/BFDaNIXe7vGk7uEL+iOheK0zzKAqkkxY8hQBlwqEBeX4BJmilZ2uNRuX6UeGYW8gWnNelD85KyDikAgy5uzv9xslNRvUiU217PwtOW7TQq4VlQioABRTIrTQzNc1MeU9eD9gNacHVPzper9dBH3mVj6tJlsMqwJB7yJKnyzg5vTXAITRaYZrHwdV8n8/Xpa7IhFQgjAKAnOZhp83gSo/foGVXGQfHD1D9EcKCh+mT3CQVCFKAIfe6yZKrFAUVSdqK39cjsqtwLfrgaEiPtOBJOx9ZcyBAbs7H3JUUd8mnWPCgn5l4QalskICn+CRl6OEVyMld8brp9VkpMuU9fj+7KCrkIS14v9fpZqjostnJVYDuJJIlJ6RSYMrBrK9HAVyFG71nC45MZZE+eHKhyLajMeQ+jz/phtzvy4MPrrIMbUULzoDbs0102Z/kKgDITRZj0g253+8Huwy40mm24KoCTqezBeZezglXJZGJQSgAyA0EOSx5MgLcfp+7p0V7LLbgTL2/tbX1tLaQXJcKDEYBtuSD2Xcw+zg6u8EuflHMs+KiYAVByTx58mSdvMgMCCL/j12BpEFO9Laed9Yxx70t72ELjnUF8DfffFMC3quOjOKjAEOeyMEVfOyqevtFLeD9RlH8GzZs6Ozu7r5E81FGxqd7shapAI1mwCen5zz93sT45H5Pz6UTey52ktb9XBTor1jv3o1+u91+RropkEWGeCoAyI0EebyHV8Bqt8t7htoKuEMCjn4AcqWAw+E4jQwZpALxVkCBnG7tK5DDZ4nT4nb5wawIuNJ09sFFC+7DSIq04PE+tbI+VoAhj5dPjiHCbrsXgNNTGMEWXBwHVyFvamqq48bIWCqQCAUAeQ8ZcJoBGHP1+KF0NHvALCw4c6zUG8qC+995551DNOkq9iPH3HRZQTYrgJuJmKQVa6CvOffUfNhwiOoRXRSFXwYcx0AGCvjoI7CX2tvba6WbAllkSKQCsUKuXGB2eWsr37twidopuihKs7WAM+Te5ubmSgl4Ik+trJsVYMgHMz0E/ndXm6eS6qI3E+m7KHwsxYLTiq+2tna3BJxlkXGiFQDceDe5EiMd6UIPzLU0OneD2d4FDKtBz4L7li9fXkmzs4IKq3vJhFQgAQow5NFUTYT696w5DwsuuieK/416QgGu/BLWrFnTSn54jbTi0cgty8aqgAo5rj0HWHB7vtvhqTnyUVMrlWYLDrhDAo62YQOsNvwZLw0X7pGAkxIyJFWBgHsy8CHhf9tb3Pj4q8IrxWBXhRs1iBYc6ww4fg1e8sN3ScAhiwzJVoAhJ1dc/2YnNaq5oWsXRQBcdFHU5oo3epAJwBly79NPP73npptuchUVFRWoe+RgwnXRZmh3YBQqu4OuMQuyiX0a6GQHCuhs1D1GX7VBKVhp7APgtfvSS4dcm1bUsQVn6x105FCAo6Diz3z00UedjY2NW6dMmXKL0ag19kHtiHil9nyj4b2dVYaOLvEVFhHvnpKCXs9XDUa3m44dpF1QW7TiB23UWdHdR+cw8DlDBlCgE/S30A5h9tOpLubsaG/r6JWnJ+gNrtbmrRdO7sYMQva/GXK1nVrAsQGaoCDMvufgwYP/N2nSpLgB/sJb6w0tHWhTZgV3t4teidBNjYbkkEhPem2/uCyjpt1fu127f7LLa4+vbZ+2Pdry2vWB9tdu1+6vPV6gvN/vMzTUHv8/Ku2hBaz2gxs1hTLLqIEB995///07aPpss661QS1RhEyEG93LtxbQKxH4+7gQPdLAZRGLad5fzOMyYoxyXEZMi2XEtFhGTItlxLRYBmltQFkE3ofTSmYE/w20v3Y7H4dj7fECrorP42mu2rZhB23VvcDEnuEAh9n30MxC5/nz59+LF+A4aKaGAOT5wbxpO6M9X9jOedqyqVjntujxo21Tostrjxfheldnx3tuu91JxWHBQ15goqpQgCMfFpytuGfTpk3v0PtSkJ/zAZBbLL2QMyRiDIUYCjEtlkllWmyTmNZrk1hGTMervF49YfL99JbNpvrat6k5onsCXvsFPcDhpuBXofjhS5cuPdzW1nZKWvGAfhaGvJ+cMiPRCoBBj8t16tCOTUfoWAw4WAWz/UKoi0wUQmGGHJW4aUx8TVlZ2YP0DR9sz/kAyBG8HsgjQ7IUAOD2jvY1dDwMa0F8hjsk4HoWHO2FyVet+MqVK9+hh5Hl2YQyvQGQm/PJXQkXcKcCge9YcFrJDPFftOVDVJFWWdH2Z4Dy9PpjT92R/e9QH8EiPAwwGtI9oXxdHxzb8ItgP9z90ksvNZ45c+ZdOf8K0vQFC42sKJAzwNoYRfmkiWltOV4Xy4hp3p5psdgHMa3XD7GMmKbyALKrs/3dMx8faKQkLDgAB6MhrTflRww4fi3uV1999QWy4rq/FlSYi0GB3GLJxa4ntc9+r9d/5tjHL9BB2T2JCXA0PsiKP/nkk7Vnz559X1rx/ueVIQ9z8a+OJMsygYGmaHTAXVdnZ+f7x/bsqO0FfEC4cZbC+eDYDmutuimU7v7rX//63x6PR/dPAnbK1QDITcoQYq/fDSHwp5hjMR3I7b9d70+3Xj7XPdj6Yt1f266B6htou7a+3nW6c9lTf/Lwf9PuuJ0suidhPYqBAEdzUAEcecVN+cMf/nCMrPg2acUhTf9goYtOk5ncFT5RKII0B07rbedykcax1hfr/tp2DlTfQNu19dE6Rk4c9o5th3d+cIxW2T0Je3HJ1UQCOKy1Cjilu9etW/c8WXGuQ8YaBVTINflydXAK+H007+TUyedpb7begA9MDuhJRAo4W3H8erqXLFlS3dDQsEtacVJDJ0jIdYSJMhvW29nVuevAtvXVtCsDztY7LoCjSWzF4dgDctfatWufkSMqkEY/AHIzja5gLjMHTgcm9AfyOQ9lOM3bo43FOsS0Xj1iGaQHCtG2D/XxPmKa2yPmiWne3uP3+Zvqjj9D21y0gD0wGJH1pnIDXmSiDAIAD7rYfOihhw7Su8TflnNUFH10/zPTRafJbFZOMp9ojrETp/mEinm6lYbZEG192vLcDr042vZp69fuH247psR2tDS/XbVl/UHaj613RKMnLFEkLgqXZSuuXGzigA8//PDT9GByK/6MyKCvAEOuX0Ju0SoAprzd3a3VO9Y/TdsY7oh9b64vWsDZF8cBXZs3b75UWVn5Z3nByXLqxwHI5c0gfYWCt8B6Nzde+HPj6dN4VhDuCZiL2Pfm2qIBHPuIVlyB/Pbbb3+THmurlhecLKl+DH9cHULkYuyfI45kwX68j5jmfcU8Mc3b4x2LxxDTescRy4hpoTwezXN1dVVvfeuVN6kIwx219Ub1gwGcrbhysYkGrFq16jGXy+WTrgokDR8UyE00iZNPKIojHWngsnr7D7Q90uNEWi7a4w1QHgz5vF5f3ZEDj1ETADdfXEZtvdGFaAHHPgw4flGKFX/00UePHDt2bDW9kRbbZRhAAYYcWMslWAMDPcxgb768mm7qYL631nqDvajCYADHARhytuLOBx544NnLly83SlclMv0BuZFGV2ToU6CH4HY7nI37Nr/3LOXicTSt9e4rHGFqsICzL66Oi+/du7f5tddee5g+f+KRrkpk6pvplr4CObsbORwDKBpy9pyuqX74YkN9M60y3FGNe2uVHyzgqIchV604DRvuI9CfpRtA2uPIdR0FFMjlU1L0pQcvjZo0PHvggw37SCqt9QZrgwqxAg5XRbXilHbdcsstK+vr63fIG0CRnw+GPFf9cbpbaejqaNuxZfXylWCod+G7lmAsJYDjDOLgWPiCE788x4MPPvgb8sc/kf44qRFhCECeez45/O5up/OTqo3v/oakwuvOwBBcADDFfFFycCEWC85HRCMwfIJfHBrnpBtATa+//vqvyB/3Sn+cFIkw4Ja+URxCzHKfHGaZ/tJ76SmdX9FrIJpoVeGHYrDEw4KUHHyIB+BoJxrDrorSSHrVRNWePXuegz8uIY/8BCmQG7P/zQVgAn735aYLz+3fsq6KFBLhjunCUlQ7HoCjPoY8yFVZtGjRcpqQtdGtvLhSPKxMh1MgYMkBefZ65TRJ0NDZ1rpxy2vLl1NHta4JDCaYijnEC3A0hCHnURU02rF48eJ/oYvOvRLy6M6ViVwVoymepye64yeytI8sd1dH+94tb6z4FzDSu/CwYNzgRh/iqSAAF/1xNNhBX4nouPPOOx+iByRq5KQsSB55YMizyRXHiEm3vbNm99o3HnJ2dHSQGgBchBsMxcV6Q+lEOHtonNjAPILcW1dXt3P+/PkLPth/eGgePqclQ0QK4L3synvBs2BKMmYIuhz2c/s2rf1ZY33tJyRAFy0MONzbuFpvCJwIwFEvQhDo5It3d3Z27naYCm6mGXWFPNE9UFT+H04B/vhAgPHM9Mv9fvpglNPZfGjnpntOHzl0lvqrhTuulpv1TBTgogVX0wcOHOiix7j2Dx899is0HJYvIefTMHCc1/uFjUwckcL9EHphpv34gY/uq9nzUU0IuGG5AXjcQ6IAR0MZbI6VxjfV17UVlQw5OqR0xEKah2GRkEd+TlXIIW2GGHK86tjtcjnqjx74pwNb38dteNFyx228W0/FRAIuHhOQq6BfqD1+0WIp2Dds1KgFNCRmkz65KFX4tAp5Bvjk8LndDkfriQN7fn5g6waMdTPcGPNOONxQMtmAq6DTnasWn8e1s7R8wvVkyEv4xKFRMoRXQDUIiiGnz16n4b8eGud2d9kbqnd+cC+9bu0o9chOCwBnuHEzJyF+N9WrhmQBjgOyBVchv9xwobOro3XbqPGTrjVZLGV8MaW2TiZ0FQhATlKyqrolk78B49z0HsGT+zatua/uyMHT1AKAnXS40fNkAo7jITDgStx++aKz+cLZD8onTZ1NryEeA59c+uUBoQb6X4UcBdPAJ8dwJt5CRTMDD+xY88YvGs+caqCWad2SpFhu1i7ZgAfBTY1Q1umdz+7zp45uHXfFjCn0AstJeUYJOZ+ggWLVXUmxKcesQHqWkm6/t2zd+saKJW0Xmy5T2xlu3MjBDMGkwg3tkg04jonQD3S60vbVVh/cPmbyFGu+rXA2+eRkyGGWZBhIAdYpYMST75H30Bg3fcqlp62p4eWNry7/N3rVWju1WYQbF5RJhxu6pQpwHJsh5wsN+nit13+quupA4ZChNSVDh881mkw0wiKtOcQaKEAnCJrMoMwIpJESj6Orlaa8Ltv2v6++QeeQLybFO5QpgRtapBJwHJ8hR6yCfuFUzQX6U7d5RPn4K8kvL5cuC6QaOKiQJ8EfJ2/bgItJR3vbwb1b1v3iaOX2Q9RCWG223HBJxKHAZP/+FMHSBXDAzYsCPV18dp06eGBjecVkk7Ww6DPSZVHO14D/sbsyYMEYCuDOpNfj7mlpOL9yw6oXf996sQFvn4LFZri1k6diOFpsu6YacLSef9lsxVXQ6c+d79ShqoN05/NI0TByWYxwWWjAQPrmYc96nz7xNeWBhxRofNvpbDl7rPpX2/73f97qdUkYboxx88Vkwm6/h+28ZmM6AI4miZCLoCvp86eON9ibWzYPHVk+mlyWKXBZMC7WdyI1vZKrvdqwrLEJArAxSoJvgna0XNpctXntkqOVHx6mWtliA3DtDRwYqpQHkJJOAe3BXFr88PCmSist+OKqjZZCpK+55bYvVEy78pcFRcUT8eRL3zAZbZWhnwIAM5bAw3/dXfaz9SeO/vve99fsovrggjDUPATI011xwPj8smJpeO++6QY4mhUwzwHI8Zg5vrQKyBl0W0FJSfENt/39D0pHj73LYrVayXWR1pwE0guBGYjRMaeOkNBDtc1NDSs/XLP6FVdnJ0ZIYKmxAGz2tTFKwnO5ozsQ7ZjIkC4uiraPLBJiLLAK7NP5vG63t/bQvkMOR8emoWWjx9Fr0CZKt0UrYd96nyvHtkM/xhwudkfsra0fHtz6/j/v2/zuB6S5OPwH6y3CjfMT25+KvubGNZWOFlzsINrHLgtbc7bocFsUyz7vq9+6oXzK9PsKCgsraE6L4rb0nVSxutxOByx5aA3Yz/aRn+1yOOobT598dte6N7dTabbUbLUx9Mc3btjosEEKXXkKc9MdcJYGkDPo8M0BOfvnCuRms7lg7uJv31Q+ruJOa1HxVLzcEv65BJ0lDB0z2LiAJD/7VNOF+pcr1/5tE72uhMHmmMe1RV87La222NNMARxtZmsO0GHNGXQAzrAjtn5x0Te/VD556p0FxSVX4Y1RmIorQSdlhKCAjfFsL1lse+eRptOnXv7ovbd2UBGAjAVgcwyLDbD5jiTATlurTW1TQyYBzo1Gm3HtwKDDmrNFVwCndSW+5uavXzNu8oy7CocMuRpfVgi8hiF3hxcDLgpm/GFilMfg6OjYf+H08ZU0MrKXNGOwxVh0RwA1X0hSMjNCJgIOZdFuLKLbAovOQ4si6PlXz7959tipM28rKhm2wGzNL8TrGHLlopShxoQo3Fr3drsdXZ1tWxtO1byzf9v7uL0OiEWokYa15kX0szPCalPb1ZCpgHMHGHIGnV0XBp0tu2Lli4eNKPrc/C8vKC0v/0phybA5NI5uogldivuSbePpGAkB3JifjU+CODrbqlqamjYc2LZxq73tMm7QsHUG0Aw5Q83DfrDaGeOOMBRinOmAc1+0oPONInZfxFix9BOmXjVq+py5Nw8rG/kVmp47lV+XFvDVM8+NUS11H9R0S91xqq350oYTVZXvnzt15CKJxQAz3GKMbbDWbLEzGmwRDE5nQ8ygIwbksOhs1QE54NbG5qu+cMO0cZOmXW8bMvTqgsLiWQS7FW95hc+ersAHA+1XXmRJlrqbXqxz2NnRvv/CmZMfHtm1/ST1F9YY8ALmUDG2s8WGC5IVYFM/lJAtFpz7wzH6xbADdF4AuBZ4xaJTvrKtoLjYOuvaL80qGzPx8wVDSq622Yo/ZTSbLLhbqjwzqsxPp9JKCMiXqBGaAMQ4UMD1xU0Y8jsMmM2HJ9ZpLprH6bQfc3V07m9uPLvv8J4dh112O1wOhhpA88KA8za21myxldqpfFaFbAWcT5IIOvx00VcXrTtDznkcm4aWlRXOuGbeZ4aXjfm0xVYwMT/fOtFsLRhnwsMYyvCjUQG/76KVD62NtVIHoNWWUiAmoHFRCJAVX5pi+oKdk+zzBbe7+6zH6Trb2tz48fG9O6vbm5txVxGQsiXmmMFGzHmIGWjRvw7dGCqc6UGreqb3J1z70VcAzjFbddGVYbC1sVhW+aFUzPzMqNETJ1YUDyubaLUVVeRbrRNMFnOZyWguzAvAj9fToZ6AmwPLjxUKCk1EMltoir30OJOjhyD2+b0On8fb7O7uPtft7Kq3tzWf/eTs2fr6mmr40AylaH1FeEOlxbLsfnCstCeb/2PNs7mP2r6hz+ICeNmycwwwGWqGnde5jBgjjUWsN4/cHfPQ0lFF9PidzVpUWFRgK8KMSIPL2eXo7qLRuvZWZ3vLxS5yKwAmuwgcM8yI2eqKMdIMNEOMdbEM78t1ckzFciPkIuDimQ0CkjYwqByL8IuAY7u4jcujPqS5XkoGpbEuBhE4TrN1ZcC1wDK0DDWvc3mOuT6OxePmTDrXARdPNGvBcCLWgsvrDDEgRzmOOT9UXTgW5wM6BIZPuw5IkQeIxZjh5RjbxDTWsSBwHFjL0f9Z8BztfthuszaIwy0i1NpyOIBYj3hABlCMkdYuDLs2X1xHvViXQaMAi6/Jlqs6CrBeDDKKhcsTt+tUGQQmQ8rwYp9weXp1yvxeBfjkSEFiU0Cro3YdtWvzGFzxyNo87bpYVqYjUOD/AZrbm7Ts1rpFAAAAAElFTkSuQmCC', // @suppress longLineCheck
+
+ red: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAAC4CAYAAABQMybHAAAk/0lEQVR4Ae2dCZxUxZ3Hq8/pnhkGmOEQuQS5VCTxWHEDBlyNkciakMMkxujGuOvHO24IKCae0UQlKwmyroocoqtozGpA4oFiVAQU5IaRcchwDsPczNF39/5/b+bfVL/p7ume6bur+DyqXt31r2//5//q1XvPIJRLpgQMMVYeiDGfyhanBGKdgDirzYvsLLtIPguB0/lc7+vh5vNIvr68Oo8ige6EH6Vo3iSxjODrw1rcm2++2f+MM84YV1hYONRkMhUZjcY+OAKBgOZTXDGVLTYYDH0gNYpvIa/V5/O1+v3+FopvgY+D4tra29uP7N27d98VV1zRiOy6g06DcRyGr1wYCfCEhUnK2yiWiQYvSQG+8ac//all3rx5o/v16ze+oKBgnNlsxjGWQB5LgA5MhrToh1BL0Fd4vV4c+1wu176mpqYvHnnkkf0rVqzwUJt+OuQfALrBmh/hvHc8mfkuCBlmDei5c+fab7nllguLi4svttlsXyeYzyPgrJkgKPpBuQn4LU6n88PW1tZ1ixYt2vjoo486qG964PMe9nwGnKE2EhgG0tDWBx988HzS0BdbLJbpdEymeFsmAB1DH5wej2cTHR+Qhl937733biYN76ZyAFyGPoaqcitLvgEeAvX1119ve/jhhy8nLX211Wq9lKYWtnIuuFa3272WtPv/3nPPPW8tWbLESYPKS9jzAXCGGj60tfGLL764YPDgwT+x2+3fp/MyOnLZ1Tscjj/X1NS8OH78+E9poNDoslbPaTMmlwFnsDWoN23adNq4ceN+TBeIP6FVjbG5THSksdEKTQVdqL64b9++lyZPnlzVCboMe6SiWRufi4DLYJsqKyunkraeCxOELhJzcbxxw0cXqQGYMKTVHz399NM/pgp8dOQk6Lk04RiLpq3JNx04cOAbAwYMmEurH1PoXLkIEqDVmPV1dXWPjhw58l3KwqAz7BFKZU90LgAeBHvYsGHmjRs3/mtpaekcMkPOy55pSH9PyXzZ0tDQ8NiFF1646vDhw17qESDPetCzGXD0XYObwLYQ2FeVlZXNoRsvZ6Yfl+ztAd1Y2lNfXw/QXyHQ9TeTsm5g2Qp40BTZtWvX+aNHj15AGvv8rJN+BneYNPrm/fv3/2LixImbqZuy6ZLBve7atWwDXNPYNAzT0qVLS2fNmvUA7f+4gS4eAbxynRIwHN8ozDseFQZHTW9l4m/3BBa/8nnDfT97vKqBKmPQs2ZpMVsARz8ZbvPRo0ev6d+//yNkjiRlD0hvqUh3ecsrpwtD2+GEdcMfELUNbWLewNniBaqU7XNAnvGgZwPg6KMJx44dOybSstYf1coISSOKsy4tiJLa8ySPX6wvrxN3TLpX7KJaoM1xZDTkmfynPaixJ02aVEBX+PdOmDBhg4K754D2tqTFKKacPVhsOPEnce+kSQK/IjMdYChjFWWmdozhNq1evXrYtGnTltKNmq/3doLypXyyNLgsP49XfPhOpfjZzCcEbKGMtc0zEXBeITFXVFR8g5YAn1O2toxW9+FUAI5ewDY/UC9+PvrXAjeJ2DbH2nnGuEwyUYJae/r06bba2toH6O7a6wrujGGlS0eMBjFw1ADxetMT4oHpZ2lbi3GtlFEmS6Zo8CDca9asGT516tTltClK3WLvglRsEanS4HJvXF6x/v0vxHXfWigOUXzGmCyZADj6oK2S7N69ewrdtFlJWnuALDwVjk8C6QAcPSSTpa6iTvxwwm/EejrNiFWWdJsoDLeZdv3NHDNmzGoFd3wwZ1JuMlkGjBsoVlf9TsykfmGFBYorrUo0nYCjbQjAQjv/rhs+fPjLFLbToVwWS4Boto/sL14++ri4joZhoQNznDbO0tVwEG66K/nLIUOGPEW32/GLVy43JGAeUiKeqvsv8UsaTlohTwfgGtz0EIKV9iE/RjsAH1IPIuQG1SGjCAhDWaF4qHmBeGzwYIG3EaRFk6cacA3uGTNm2OnFNs/16dPn1hChqJOck0CJTdxaeY94bsZkzfxMOeSpvADQ4B44cGAB3cBZRg/8fi/nZjNDBpSuVZRow3d4xGsjHxT/VlsrXJSPlxGjFUlIWqo0uAY39dhaXl4+X8GdkLnLqkrsFvE90uTzwQAdKdPkqQCc4bZUV1fPpXeQ3JhVM6M6mzAJ9LGJG+v/IOZShSm78Ew24DCB0Ibl0KFD19Ozkr9JmLRURVkpgdIi8Zvqx8X1YIIOsJFUMzmZgKPj+FNkoQvK79CqyQIKK6ckIE4pEQsqHxbfIVGwJk8a5MkCnOE2b9++fRrdfl9CS4GAXTklATwiYRpVJp7bfb+YRuJI6h3PZAAOuFGv+a233hpPb5NaSeHkPGJCFSuXnRIgSGwTBomVb/2nGE8jAORgJuGaPFmAm+iB4KIpU6Ysp70lJdk5BarXyZaA0ShKLh4tls+6QBRRW/gLn3DAE10hfjDoaMHx48ef6Nu37w0UVi7FEsjEdfBoImh2iMX97hR3Up6Er5EnUoPjx4L6LHv27PmugjvalKo0WQJ97eKGLx8U3wU7dIChhCneRAHOcJtXrVo1dtSoUQvlAaiwkkB3EqAngxauuk2MpXwJtccTCbj5kksuKaLHzZYpu7u76VTpegnAHr9svFh2yQTNHgfkCdHiiagEPxLN7qbnKOeXlJSoO5X62UvxebbZ4LJ4yB5/muzx2RSXEHu8txo8aJps2bLlYtodqOCWZ0uF45YA7T68cfu94mIqmBBTpbeAa9qbzJIiWu+eTy9Wj3tAqoCSgCwBIETr4/OnjwsuHfaK0d4UZu1tef7552+nF/OMkzuqwkoCPZWA1SzGvXS9uI3K93pVpaeAM9zmF154YdSgQYPwaJJySgIJk8DgvmL2C/8hRlGFvTJVegM4Liwt9HTO78k0KUzYyFRFSgIkATJVCq88S/yOgr3akNUTwFl7W3bu3IlvTH5LzYiSQDIk0KdAXEEbsi7vhBysgr24XE8ARxnzlVdeWUw3dB6LqzWVWUkgTgmMHSgeu3Ky9oFeNlXiqiFewFl7m5988slb6XPXI+NqTWVWEohTAhaTGPnMLIGH0xnwuLR4vIAjv4k+gV1Cr3u4Kc6+quxKAj2SwIA+4qbrpwjsSsV1X1zMxpOZtbdl3rx5P6cX0Zf1qLeqkJJAnBIwmUTZ/TPFz6lY3MuG8QCOvGbc1DnllFNujrOPKruSQK8kQG/Kuple0Yx942yqxFRfrICz9jY/88wz15DtfUpMtatMSgIJkoDZJE5Z9mNxDVXHgMdki8cKOPKZ6Fs59lNPPRV3mJRTEki5BIb0FbdNOj2+N2TFAjhrb8vKlSuvIu09IuUjUw0qCZAErBYx4i/XiasoGLMtHgvgmvYeOnSolY47lKSVBNIpgWH9xR0lJcG7m93y210GaG8c5ldfffVfaEPVmHQOTrWtJEAbsca8f7O4BEzSwXxGFEwsgGPt0Ux3LX+ktsNGlKNKSJEEsJ121CDxIzBJB9gE5BFdd4Aj3XT11VfTM8R9Z0SsRSUoCaRQAn0LxIyrvyb6UpMAPCrD0RLxy0C6ee7cud+m5yzV50VIGMqlXwL0/Kb9nsvEt6kn3S4ZRgMcaRrg9P2cHyjzJP0Tq3rQIQGYKSP6iR/QGQMekeNICdDeOExPPPHEiMLCwq91VK3+VxLIDAkUWcXXnrhaYMma7XDw2sVFA1zT3jNnzryKtHekfF0qVBFKAqmQABFpnDUxRIvHDbi2ekKPo30/FR1WbSgJxCuBwcVBwFmLd6kinGbGLwHxphUrVoynte+xXUqpiIyWQKBoWEb3L1GdozXxsS/9u/Z2Wl5N6aLFYaTrXRDwr371q9PUxaVePJl/3nzef4uaN28S7hNHunQ2EOgSRa/r1rkuEXild1enr6unecJVHktd9OlwaOJp1LPddEApg92QotEANw8YMGCqApwktmen8K9cIURTI8kv810BdXGI72JR73LR9+ND5jvzOx9nD80u11QhVj1DxRjwkBr0gOMXoGnwoqIiM72p6sKQ3Hl64nv0fhGoPZ5Vo8ff7P5+v2jw+Eil5S7kfQKBC8FqW1sbAx6ixRGpd4gzLVu2bKLJZCrVJ+bjebbBzXNkoTsipfRQo0HTWRybWz7BWvqHkYMn0qjYDg8ZoB5w1uCmM8888yJlnoTIKitPGHIj3R3hyc0lHwCPLbRdRB4A56EF5yoc4Igzkf09RQEelFNWBwB5f3okJhfnE2MqNZumgFk6wC4gDzoZcKbfRLfmrWR/Tw7mUoGsl0Ao5DzVueEXmUyThxcW8heUeVDanMmAIwLnxoULF55Nv4w+Wg71X85IgCE3AoEccjScPr8ZderZNCSNX3lo8ioKk28aNmzYhFz8cyYPPF/DHZAbRKPXmzNrK6B6qM0ygbxP6WCOtaUjWYNzgpH2fo/JVwDyYdxmUuH9zWZN3eXKePuYjGAWPDPH2tD0GhwZTLR7cIzS4Jp8cvY/QN6PIG/KAU0OVouMxtPBLh0MuTZ3rMFBPRzOjQT4aO1M/ZfTEmDIc8Emt5s0wDV+OydNY5oBR5ym2ktLS802m21kTs+sGlxQAoC8r4nMFZp9DQAGIct8m9EwstRuh0XCw9DGqAfceP/992MrGrYzKJcnEjgJOdjIUhcQBbcPHQx2wXRwIGyDM/XGs846S22PzdI57k23AXkJmbAnfNm5dwUAn1mkbe3+ohNwRAVYgwcBHzhwoLrA7A0pWVxWg5xe5Wo8qQCzZjQAuNRs7rKSwhocAwHsRrvdPhwnyuWnBAB5H9LkLZomzy4ZFJmNYFfjmHuu1+AGevdgMScqPz8loEGuafLsGr/ZYAC7bI3A1x6751EgwkhbZIvVGjiLJH99QF5Mmrw1SzQ5mKVFcAAOpa3BjdnjkyD1BDheMq6ckoDQNDntQsQSYjY4ghzsBllGWLbBMQaDAhxiUI4lYCLNWEzmiqbJM/zBIKvByIBz9zUNzica+QpwFofyWQIMObGe0c4kAgx4sKeswYMRCvCMnsO0dY4hb/P5M/YZT7NJ0+AsI41pXkVBJCKUicLiUX4XCQDyIhNWyYP6sEuedEZE0+DcLwU4S0L5YSXAkLdrmjxslrRFGmOxwal3bLakraOq4cyWACAv1DR5ZvWTVlHArmaJcM/YRAn+zfF6ve2cqHwlgUgSYMi7rDNTASYs1b7PH5DZ1Zjm/gXH4fP52oInKqAkEEUCgJz2YWeMRU6Xv3p2NZWO1c3gCqfS4FFmVCV1kQBD7qS3aKX7LXE+v1/W4BrXbKJwxwNKg7MolB+rBAC5jd69Ql5anS8goMGDyhqdkS8otQQFeFrnKGsb1zQ5Qa5p8jSNwm8ImihByMNq8EC6/9akSUCq2d5JAK+H0zR576rpUWkwSyuXETW4Zq9QzQFlg/dIvqpQpwQYche9vDvVb7X1BgRs8CDL6JKswbUEAry1s6/KUxLokQQAeQFtQUz1HU96FzrYZcC1vss2uBbhcDgaoO7VnvAeza0q1CmBDsiFcPlTIxJQ7aTXoetbYw3O1PsbGxv/oc+kzpUEeiKBk5q8J6XjL9Pk9YBd/KSYZ81EwQmcFllRUbFfXWR2CET933sJAHKrZq4k9w4nelrldOwnLwg3wqzBka4lvPbaawpwSEO5hEkgCHkS18kB72v1zXrAg+vgTL3/7bffbnG5XLVms3lgwkaoKsp7CQByC0nBo+nRxIvD7ffXrjve1EI1dzFR0FoQcGRobW2tUmYKxKJcIiWgQU6gJ1qRg9U2X6CK+gq4wwKOcQByLUN7e/s/EKGckkCiJQDI6fUOCd9x2O7zgVkZcK3rbIPLGtyHlRSlwRM9tao+loAMOcf1xge8TT4vAPfREaLB5XXwIOTHjh3b35sGVVklge4kAMhhqngTsC0E9dR6fGA2BG70IZwG97/xxhs7aNMVgFdOSSBpEsDNxA5zpXdWuY/MjVW1zTuoowA8BHIGHIMA0Ej00Udga5ubmyuVmQKxKJdMCQByE/ENfd6Tf6C2xR+ofPFITS31UzZRtG7rAWfIvfX19ZsU4MmcWlU3SyAIeQ8UOYCt93g3keelI0R7o34ZcJxrGpx8X2Vl5UYFOESiXCokAMgBI3lxHTDkqxyujVQU2ps1eLDLMuD4MbAG9y1ZsmSTn1wwpwooCSRZAgx5PM3Qg3L+JTX10OAMN3OsVRMOcO2XsGrVqkayw8uVFo9H3CpvbyXAkMNa6e7AQ6DNXl/5W8fqGyk7a/CIgKNvrMFhz3hpufBTBTjEolwqJQDIAXd3DrDWuj34+KvGK/lsgweLyhockQw4fg1essM3KMCDslKBFEqAIY+mxdGdynbPBvIAuGyiIElz8o0eRLB6xy/Bu2DBgk8vvfRSZ1FRkU3Lnaf/VRaVCM/xmpwffSRlBijCuUjxyBsxLUJCpMfbkB39AvD6/jn8fufjh46wBmftHdJCOMCRUbNnPvnkk5bq6uoPRo8efbmRnphOhGvbWiGO/c9fhaeuORHVpaQOt+8rwlmCb7uHyC6k7UgpUctEKBStTEijnSf6iZfzRGhCyxJvO3K96Q7T42mi2nnig21N5dhByPY3Qx7snh5wJEAmmgYn37Nt27a/nnbaaQkDfP+dTwp3dT3aySrn9HtEu9+r2YYQUCw2IgbIeRk0lOO4cOmIk12q88ttI8x9jdR/fX79eXfl9en68pHG7w34xW5nzV8pv4cOeQ08pIpwahltMuDeW2+99SPaPlsfTUuE1NjNSTbCjSEVGS2i0NihD2KFG+U4L3w5jDQ4OY7zyL6cRw7LeeSwnEcOy3nksJwHYb1DXjguw2EtMob/uiuvT+d22Ne3h3iw6Az46he37PyITiNeYKJsNMCh9j20s9Bx+PDhvyUKcDSarQ6Q2wnyaNf4nMa3nTFWjsuEcXNfYu1fsvP3RCbQwLU+598a3W4HBaHBw15gou5wgCMeGpy1uGft2rVv0OskEJ/3DpDbjCYNWoZE9iEghkIOy3nSGZb7JIcj9UnOI4cTlT9SPdHiAeZ2Z93r5MnmCaK7uEiA40eCXwWo9sydO3dXU1PTl0qLd8iPIe84U/+nUgJgsC3g+XJJ8+7d1C4DDlbBbBcX7iITmZCZIUclbloTX1VWVnYnfcMH6XnvADmcKwDZKpcqCUBN13jbVpHnpoPNE+a1SzciaXBkRF1BLb58+fI36GFkVKhcpwQ0Td7lS4xKPMmUgFv4PG+3HXmD2pC1d1jzBP2IBjh+FSgIM8W9dOnS6qqqqjfV/iuShuSwsmJTkEsSSV4Qa9+1Pseb77ZWVVMr0OBgE4yC1bAuVsA1M+XFF19cTFo84q8lbAt5EKkgT80kuwMB/7q2I4upNTZPegU4eh2ixefPn1958ODBd5QW7zqhDHm0q3+V1nMJkPIW9f72d149UVHZCXi3cGOWomlwpENbB80UCrteeeWVZz0eT8Q/CSiUr64DciwhnnQcjnbjArk5PV6fy3KL+va6q6+35fX1d1dfd+n6+vjcL/yBjx3Vz1J5Fx2yeRLVougOcPQHFeBiUzNTfvvb3+4lLf53pcUhmq4ON4IKDB2QY3Lg2JfDPHFyHMLxOq67p/X1try+v93V1126vj6cd9jezr+vaCrfS6dsnoDJqHCjbCyAQ1sHAaewa82aNc+QFkd55cJIQA85w5cKH91hiORwKtpOVhs+4nij89gzNB7W3oAPTHZrScQKOGtx/Hpcc+bM2X706NENSouTNCI4QG4lTZ5qx3AzbGif41Ldl0S0B+1d73dtWNy4ezvVx4Cz9k4I4Ogna3EY9oDcuXr16oVqRQWiiexOavKTiOEyC44vtzisRXbGcxznicfnsrHWp8/P5SL5+v531zd9/fry3aV7aOVkk+P4QsrnpAPsgcGYtDfli8lEQT4ADi3OgLtmz569jd4l/rraowLxRHY2TZPjY6kd/5CTJ1kOR0qPXHP4FK471vr0+blcJF/uc/gehMbq69eXj5buoy2xR31trz/duGMblWPtHdPqCfciFhOF87IW1y420eBdd921gB5MblR7VFhE4X2GPHyqig0ngY49J97GxU27FlA6wx2z7c11xgs42+Jo0Pnee+/Vbtq06U/qgpPFGdkH5FhdUS42CeD5qb2exj997qzFG6tgnoC5mG1vbiUewFFG1uIa5LNmzXqNHmvbri44WaSR/QLaZstLiJyLrXP4sRwox2XkMJeV4+Qwpyfal9uQw5HakfPIYTk/tHej37X9vuMbX6M8DHfc2hv19wRw1uLaxSY6sGLFikecTifegYg6lYsiAUCO1RWeUGRFOFbHeSOV7y491nZizRdve93lB0Nu+qD8O22HH6E+AG6+uIxbe2MM8QKOMgw4flGaFn/ooYd27927dyW9kRbpynUjgSDkeP+HOkJkEKBfwCF/68oXmvdgv7dee4O9uFxPAEcDDDlrcccdd9yxqK6urlqZKrHJH5BbeqRfYqs/G3NhzftEwF39ZNPORdR/PI6m195xD6ungLMtzsuGzs8++6z+pZdeuos+f0JLl8pUiWUmGHL82c73A69hcwm/5/3WQ3eVOxrw2gWGO651b73cewo46mHIg1qclg23EOiL6AaQvh11HkECgNysNDltdPKLfe6GRU837d5CotJr7x5rzN4CDlMlqMUp7Lz88suXHzhw4CN1AygC0WGi8x1y3NCp8To++lXN+uVgqPPgu5ZgLC2AY6rQOA6+4MQvr/3OO++8j+zxGmWPkzRidJq5YuiNvomxoQzLBru72e+pWdS46z7qWjsdYAgmAJhivijYM5cIiaITWD7BLw6dc9ANoGMvv/zy3WSP0zeGevzjo6ryy2H50EKQR7pNnmvx0MvugN/7vuPw3Vucx47RbGv8kA+WeFmwVxAkAnAQjM6wqaJ1kl41sfnTTz99Cva4gjz2OQLk+DBTrjswAbt7r6fhqacbdm6m8cpw9+rCUpZdIgBHfQx5iKkyY8aMJbQh6123Gz9I5WKVwElNnrurK16C+4i39d05NeuXkFz0pgkUZkL+9CcKcMwdQw6acaGATrfPnDnz13TR+ZmCnKQRh4OpYs5Rm9yjXVS2f3ZX3YZfk0g0TsjnZcGEwQ1xJxpw2R7XIKevRJy49tprZ9MDEuVqUxZEHrtjyHNpjRwrJvU+R/nDjZtn13scJ0gaAFyGGwwlRHtD0snY3obOyR00EOTe/fv3r582bdr0pmXv9MVXbpWLTQImklWHQGWRxlY203IB7kaf69CC5p037XDU4osCbXQw4DBvE6q9Mf5kAI564UJAJ1vc1dLSsnFUZctltKOuUEHeIaRY/gfkcBBotq6k+KnzJwKe+mUnym9c13roIA1FD3dCNTfkBZcswGV1Ewxv3bq1rcBk+Xycpd836c+vVUHeMQmx/M+yCgozlkIZkoe2mYrWgKf19ROVt/y55cty6pYebmhuAJ5wlyzA0VGeC/a1zm9z1jaVme17hluKL6HVAgtPXMJHloMVsqxCBJrh4+yA292+tv3Ifz7btAu34WW4E7beHUkMyQRcbhNzEpyXTY5jx+kJly2jrSXTSZPbeeLkAiocXgIsq6Aww2fLiFjY3Cf8nsbX2/bf9mzjLqx1M9xY80463BBCqgEPgr7VWdvQbvCuH28tnUo2eR+eOHRKuegSCMqKTHOY55l44F0mDQHn0eXNX9z8yomKPTSiVjoAOMONmzlJsbup3qBLFeBokJVOEPJyV2PLUW/738+2DbjAZjCV8cVUsHcqEFECgDwoyIi50pOAde46n6NiYePuW9a2HfgH9QJgpxxujD6VgKM9OJ4XzT/gOeHY7W5Yd65t0CS70TRE24nRuWrQkV39H0kCDHmmrK1gZnH7/ZjXsfWRhs23b3HUHKW+682SlGhullmqAQ+Bmzqhndd6He5PHDUfTC48ZXShwXyagpynp3ufzRUIMp0OuwLpWUq6/d72wd21G+fsdzfVUX8YbtzIwQ7BlMINeaQacLQJ1wX0Fr/b9zfnwQ/PKxhUUGKyTjIJo4Enr6OI+j+SBGQ5YcU81Qfgdga8gQpP0/O/qP/4d41eB77yK8ONC8qUww15pQtwtM2Q84VGwEsbyN9srdo60FRYPsRin2wxmOzYS4AHc5WLLoGT5kr0fIlMxY5AvL+k2e9ufK/98Lz7aje9SnPIF5PyHcq0wI2xphNwtM+Qww+CvsFRfaTa2/beuILSM+0G0ynKZIGounephJxNkhpf+7aFjTtvp5WSHdRDaG3W3DBJ5KVAzHHKXaYADrj50KCv8rS0rXFUvXtOwSBTX5P1K8pkiY0NNleSSRNu3sAkKfc0L7+j9sMH97ua8fYpaGyGW795KrbOJyFXugHHkHgu4DPkmjanP3e+Na1V2waa7buHmAsn0/ZRu7YXQ5ksUVE4adIlducKcU0mCW7euBvWOo7c/UDtxr90miQMN9a4+WIyabffow5el5gJgKNLMuQy6Fp4g+PY0cNksoyylgymz4SM7nioS9nmurkMOT0JeUh0j05ga/toiuj78OKQr/W9RY3b57x64stdVBlrbACuv4EDJZV2l2lXb+gPrivxw8OXVgvosNFhp6MQ4TvKzvnni+yn/rLUaB2BJ1/4TzKlKRdGArCVe+PY1m70uw9+7Kz+wx/rt26g+mCCMNS8BMjbXbW/vr1pM5FlMw1wjA19wgHI8SVmKx2AnEG39zfbiu8vu+CasdZ+19HHWAvM2ESqzBYSUXgHDRwv5rxC0ub3uCrI1n6w/tMXGr1OrJBAU+MA2GxrY5WE93LH2xQVTZ7LFBNFP0IWEnwc0Aps0/mcfq/3rbYDO+r9zrUjLMVDaePWCGW26EV48px//Kw5ovl4wxSbI/Ty+Y+fa97zq0WNO9aRzOXlP2hvGW7MT0aYJCdH3RHCWDPZoX9ssrA2Z40Os0XT7HMGnP/1C2yDbulrtI7E64nx7lae1EweXKr7Bq0cybGd7SI7m9a1D3zmqln0WN3nH1J+1tSstbH0xzduWOlErjhSgymKz3TAWQyAnEGHbQ7I2T7XIDebzba7+p1z6STbgGv7GwvGKNBZdNF9GWx6J/eXO5x1z/++aetaejMZg80+r2vLtnZGam15xNkCOPrM2hygQ5sz6ACcYYdf8Kuy8y86zz7g2jKj7SwFOkkkjJPBJlNv9xZH3fOP12/+iLICZBwAm31obIDNdyQBdsZqbepb0GUT4Nxp9BnXDgw6tDlrdA1wOtf828rO+afJtkHXDTLZz7XiNQxUBIXz1XwB1KASa9n0Rilx3Of4fJPz+PKF9Vs/o2gGW/ZlcwRQ84UkBbPDZSPgkKzGKfmy2QKNzkuLMujWG0rPmnRhwZBvDzbbp9sN5kLAni8XpQy1n9AG1I6At51edPnBRlf1G4sbduP2OiCWoUYY2poP2c7OCq1NfQ+6bAWcB4D+A3IGnU0XBp01u6blh5qLi27od8b0Mdb+3xxosp9PoJvwch3Anmvr6Vi/BtRegprA9tX6HJu/dDe+vbhp7wf0RincoGHtDKAZcoaal/2gtbPGHKG+dnHZDjgPSA86TBi20dmEYV/T9FMKTx00q3j0ZSOsfb5ZYrCO0UyYLNbssqbuhBpfS/jyoLvl7f9r3f/O+vajx0kmDDDDLftIg7ZmjZ3VYNM4NJcrgMvjgTbHuAA5NDprdQDO0Mu++Yf9xo2dXDB4Kmn1c/uZCibShWkBPi+CR+gy1ZSRgcbmJzxJQ0t8riafaxdp6883uWo+Xtm0r4LGDG0MeAFzOB/prLFhguQE2DQOzeUa4PK4WKsDdD4Ath54TaNTvJbWz2wr+FHfsRMnWErPG2iyndvfVHAGwW7BBSqA7/jX0QwLL1kXrYAYjg1f+LhMBNC4UCSoPfSmqL21Pufn5Z6GLS83V+xq8jphcjDUAJoPBpzTWFuzxu6ongrkkuM5yqUxyWPB+Bh0va0ua3eGnOPYNw21FRX+oHDcV06zlpxdQvtfCg2mEYVGy1CrMNpZw7Mvwy93AmG9oBlafT6GGPYzQGbfLfyOdr/nSHvAd5B28x2scp/Y+Wr7vu1HnG24qwhIWROzz2DD5zj4DLRsX0fqDmXPbqeXe3aPJnrvGXT2WavLpgyDrfflvNoP5eLiYYMmWctGDjEVj+hrtowsMliG01cayugppEK6k2qnbWCFlNGMxhh81vRsXkAbgywizUuvWWinW+QOT8DX7vL76tsCnkPNXs+Bal/rwR3u+gPrWg/DhmYoZe0rwxsuLOdl84P96BLLgdR8Apynq4O5DqWKMOAFtLIPwBlqhp3P9Xk14DvrCKmbzB3zSGtx0RBjob2fuaCoj8GKHZGiJeBub/K62qr97Y4D7tY2MisAZofyPukzzPBZ68o+wgw0Q4xzOQ+X1ddN2fLD5SPg8syGAEkJMqx6kGXA9WlcDvUhzPWiLTmMc9kxeIjjMGtXBlwPLEPLUPM552ef62NfbjdvwhC+ch0SYFkwkPD14PI5QwzokY99jg9XF1rheEAHx/DpzwEp4gCx7DO87CNNDuMcBxz7HWd5+j8LPE+HH3XYLBv40Q4Zan0+NCDXIzfIAMo+wvqDYdfHy+eoF+fK6STAwtdFq9MIEmB5McjIFi1OTo9QZQiYDCnDizLR4iLVqeI7JcCTowTSOwno5ag/R+36OAZXblkfpz+X86pwDBL4fwN/IZwMBwH5AAAAAElFTkSuQmCC', // @suppress longLineCheck
+
+ yellow: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALcAAAC4CAYAAAChOH1KAAAlaElEQVR4Ae2dCZhUxbXHTy+zL8ywDDsSVhEVJQoCkoSIIr4kvohLxO2ZfC8an0mQrCQm+uJ7qHkv5hE/xSQaNokBogkxigaUuLDIpsiOMA4MOwyz7zPd7/yLOZfqnu7p7umeXut83+2qW7du3apTv3v63Lr31rWRkUhowOZViPc6Nutpbq/8WPVO8173sYtJ6kgDusI7yme2nYdTdIZQj0NH1vrgwYNtc+bMyb344otzCgsL87KysnLT0tKym5ub6+rr62vKy8urd+7cWfv444/XlJSUAGSB2VfoKw3HM9KBBqQzOsiSspsEXgmhCMTtDGT2TTfdNDwvL28kQzvC6XSOcDgcQ2w2Wzfenme323M5nuN2uwPql/O5OF+dy+Wq4X2rOV7Z2tpa3NLSsp9Pgv3V1dX7XnnllU/4RKnj7S5edNARl4WjRnQNBFS+njnJ4wKxHtp37NgxpG/fvlPY6l7CAI/kZTgv/YMBN1L64hPAzcAf5eUTXvax9d9x/PjxtZdcckkxH0OAF8gljNThE7acVIdbQLZzDyJuX7du3YChQ4dOycnJ+QJb5M+zFR4Qr73L1v4IW/h3amtr/3nw4MG1kyZNOsJ1FdglBOwpKakItwfQTz31VN4dd9wxnd2LL7J1/hzDPCxRSWDYD7BVf5fdmbeXLl26avbs2dXcFsCdkqCnCtwCNEL7gAEDnBs2bPh8QUHBnenp6TdyWi4vySY1TU1NKysqKl6cMGHCO0eOHGnhBuqQJ71FT3a4FczcqQgdu3btGtWvX787MzMzv8YWun+y0eyvPWzRjzY0NPzp2LFjL44ePXoP52vlRbfo/nZN6PRkhdvyoX/7299245GNe9iHvoMvBC9P6N6KQOX5gvRD9tGX8gjMovvuu6+SixRrjjCpJJngRluwAGz78uXLu0+dOvXbDPW3eL2QFyOeGihnyOevWbPm6VtvvfUsbwLcAnpSuCzJALcH1KtXr+4zduzYWbm5uf/OnZWMvrQnouGv1bD8ftu2bf937bXXnmgDPCkgT3S4lZXmDrGvX79+0EUXXTSbRz3u5fXM8Ps85Upo4FGWBbt3735q4sSJh7n1YskRJqQkKtyoN8B2vPzyyz2uueaaX2RnZ9/NN1bSE7IX4qjSfMOoqa6ubvFbb7318xkzZpRx1XDxKZY8jmoauCqJBjfqi8WB4bzNmzf/W/fu3R/j9R6Bm5o6OWyuErK1fMiKwuhfJ8VNZWUVtT/77MTHFrYNI8oIS8L444kEt7ggju3bt182bNiweXwHcXwnuy5pdwPYzrofMdiR8SaaW9wffFLc+N3RE/7xEStNrHhkCu/iXkgEuFFHBfb8+fMLb7vttkf4YvGb7II4u1g3CVm8vXEpORtfiGzdbbaWmpqW3724ou4/v/WDj8u5cMAd965KvMMt1tp56NCh24qKip7gmy99IttzyVWao3ERYekKcbnpxKmy5h/3HbVpGZcvdzzj1ooDnngUnHQOLI8++mhBZWXlC3369FlowI5tV9lt1KdPz7SFdaUTXnj00REF0kccxqWRjMdKWWBv2rTpUn7YfwnfWRwZ225NnKN3peXWtdDion0799TedfkXPvqY0+GLywWnni2m8Xiz3KgPLLbz6NGj3xgzZsw7BuyY8uH34E47jRxzUc47J/eN/wb6ixf0W1zxFC+VEWvtnDt3biE/ybagZ8+ez7CysngxEqcasNkoq6i785m6w1ctmDt3GB5xEMjjwiOIh0qgDjjJMG49hp9ae5Gt9fA47c+4r1a03BJvRbS43J/s3FN3R5ubIhebMR0Tj7XlFoudtm/fvmsuvfTSNQZsb2wSY91ptw0fMzrnrYObr7iGa5zGC9yUmBrPWMItYGOY7xZ+W/wvrIw8XowkqAa4Q/M+MzjjL0d2jL+FmxBzFyVWcOO4OLPT+AH6b/ELuAs5bp4LYSUkujDg6f37Ohee3ncVHjUWCx4TzmJxUAE7/eTJk4/06NHjKb7bGIt6JDpH8Vt/N9l7dnc8dfaTcY9wJWG0YjKSEm2oFNh80ZhRVlb2NL/D+KP47SFTs3A1UFiQ9qOakglPjx7dKyMWgEcTbgX2+PHjs3j6hCX8fMjXw1We2T/+NZCTY//65jeGLRk/vjuGdaNqwaN1NavA5salnz179jl+9evO+O+WxKxhrIYCA2mrtq71xdwLNt7P+Zp4kacLA+0W1vZoWG4BO4197McM2GH1V8LunJPtuLP84FWPcQOidpHZ1XCjfCxppaWl32Ef+6GE7R1T8bA1UJDveOjUnvHfAQ+8CBthl+uvgK6EGy6PAru4uPj23r17z/VXCZOeOhro1cs5t3T7uNu5xQJ4l7nGXQU3KqzGsfmF0+v79+8/P5oTR6YOKgnYUjfZ+vdLm7/vg7HXtwEOTroE8K6AWyy2kx9ZHTdkyJAlbY1IwJ4wVe4KDTAgacMHZy3Z9vbl47h83MkEhxEHvCvgRpnOefPmFfF49lLMU83rRowGPDRgs1POpRdlLZ33xJAi3iCAe+QJdyXSZ4sCmyuVwY+truA5RKaFW0Gzf2gaiNehQH+tqKt3vZkzaAOeRWnkRZ4m9Jc9pPRIWm6cKMrPPnz48HcN2CH1Q8pmzs6yTzux+8rvsgJkiDBiBjdScAvY8LOv4hd58UyBEaOBoDRQ1DPtEfa/r+LMEX2SMFJwoxzHE0880Yv97AXsZ6OSRowGgtIAeLl0VNaCJx4d0ot3wL9/RLiMxF8AKgKY4WcvY3dkOseNxEgDieZz62qqq29dlTNo422cFhH/O9wzBCeHgptfOHjAgK13lYmHqoHsLMf0ozvHPcD7yehJWMY3HLgtsBcvXnwB34F8ONTGmPxGA94a6FuU9vDiZy6+gNPDBjxcuNXoyA033PA4+01mLmzvnjLrIWuA36jP/dcb8h7nHcMePeks3JbV3rp167X8sVF8NMmI0UBENJCXa7/xo3fGXsuFhWW9Ows39nNed911uRdeeOEvI9IiU4jRgKaBi0Zk/vK663rDGxDAta3BRTsDt2W1n3/++Vk8jfDQ4A5lchkNBK+BNKdt6OJfD5rFewjcIV9chgq3BTZ/UGkY36wxz2cH318mZ4ga4Js7Dy1fMHpYZwHvDNzqIpI/1fEkX0Sab8+E2GEme/AasNltmdO/kP8k79Gpi8tQ4Las9rvvvjuBXxe7LvhqmpxGA53TQE6O7bp1r4+RW/PgNWj3JFS4ldXmW+zfY6vdudqavYwGQtAAOONb89/nXUK23sHCbVnt119//TKelmFqCPUzWY0GwtJAbq596j9eueQyLiSki8tQ4IbVdl555ZWzOTRmO6zuMjuHpgGbbfxlOeAOcIPDoPgLBm7LavM3H0fl5+d/KbSKmdxGA+FrID/P/qWXXxw1iksK2noHC7ey2pMnT8bQXzD7hN8aU4LRgIcGbPYpV3UDf0Fb70CgitV2LFq0aAhb7RkexzMrRgNR1ADPezJj0fyLhvAhYWzBbofuSbBwO6dOnfogX7miUCNGAzHRAA+cOP5lSt6DfPCgXJNg4HawO5LDs0XdFJMWmYMaDWgaKChw3DR5ck/MqBDwwrIjuMUlwTQN0/lzHvjuoBGjgZhqwG6ngmfnDsTbXgGtd0dwY5u6kBwwYMCt5qZNTPvUHLxNA+BwYP/0W3lVLiz9MuxvA6w2FsecOXN68fPaX2wr2wRGAzHXQF6O44tzZlsvEwur7erVEdzY5rz77rtv5s9S49anEaOBuNCA3W5L+/rt3W/mynTomnQEt3JJ+JvrmA3IiNFAXGmgX1FawC+m+YJbzLxjyZIlI/mNdtzTN2I0EFcayMqyXfbS7y4cyZWSURNw6yH+4MYOjokTJ95iLiQ99BX3K271Tx331Qy7guBy0vg8WG/FKoft4IbP4i3IpPztwsLCz3tvNOvxrYEW23iqKPs9VxKfnUlc4fncPSrvtUpYb2lygU+/frc33JZLcs899xSwS3KpxxFSdMVWv4dsle+Qzd0c9xqAGevm/AJVVBSTy+ViCDwhQQN8JLVLc1P7/bz39VXOuTye+/rK5zvNcz+U1ZG4XO5Lb5teWbBs1QHMUCXsWoV4w42yYLUd99133yQ2/dBVSoutbhc5997MmkscS4hOK2hxU1mlb7h9daj3f7r3uq99Yp3GJ67jnqktk5atopVcF3Dr0UnecKNNCu5+/fpNNv42m4PyN8jWdDTW/Rjy8TF22yPLTWeriFyWLQu5mLjeAbD26eaezMHfeQG3SLJaiwRdsK7g5icAJ+kbUjVuc+OziYkpPD0Cdc/nDk0EM9xJFedn2ybyrvizEnatknS4oQIsjlmzZvXMzs6+0MplIgmrAQHcwT0tHZxMYXaGe9QDX03vCW55kaap/vIFt33mzJlXt2VUmcxPYmsAgBfmsWkD4Nz9SbbYvnq1G7yC5Q7hRgYH35W82vjbiQ20d+11wL23JfI6OO1TSIBbXBMArsTbciu4eU4Sc1dSNJREoQKcZ9+DBY+U4F8AIv8GEleJUfrJyiTw2g5uGS0R2hXcPL79mSjVyxwmyhpwwkXJdVNFTeRGUQRwNEXiEkajedkZBF4FblUN/nHr5zDi9p/85Cd92NSzh2YkWTUAwAtgwcWkJXhD+UTKm3VLWh9uhmJYmiNwo5lY7Pw8yXDZaMLk1YAArkZRuOdhaRN5mXSxDdyCZ2FZrUgPKrj55s1QczEpKknuEIB347cRYcGFiEQMUf++3V1DubcEbtVx7Sw3v3UzLLm71LRO14AADqudyJKbaQO3ArdqjQ434naeB3CIsdyJ3M2h110Aj+QoSui16Pwe4DUnm4ZwCYphDhXcGC2Rcxahg0dKkMlIimkAgOdnu6mqzvdTg/Gujqx0G7jFiInFM0iHIME+atSo9MzMzAEqxfyknAbOAc4gMBWJdnGZke4eMOozmengmBcFuA63bdq0aYV4jDDletU02NIAAM/LOge3lZgYEceUMa2FXFWAbcEtKzaen4RHP42kugYE8M6Mg8uFqVh+6FLSfOlVtnU2v+yHcFCRG/xaPOt3KG29evUyN2989UAKpgHwXH4evKZee0A6SD0IsMgucQl9FaFvk7iEgfLr27vnucGvwK38E9lu42FAY7lFGyYkZcGz2YkFLgkg+TkOsdyqtjJaomjnZ7gN3AnQidGsosOhWXDrHZdo1iC4YwHgzEzfbglKsBm4g1NkquUSwGsb4neYEG5MTjp5WG6P0RIeBswxN3BSDd3g2gvAc/irox35wsGV1DW5UK/0DDemNlZeCI5ijQkikT91jY1GjAZ8asAX4AI7Qj3us4BOJOpl6nFfRaU77AI3Ntv00RK+gDBw+1KaSTuvgXOAu6mOZwqRuUcEOuSSuITn9+x8TC9L4hLqpTqdynIjCdbbc+6t1tbWFiQaMRroSAMAnF/MpXoA3lHGKG9rddk8+BWfG9WwNTU11fqaoSjKdTSHSwANAPCsjDYTGQf1xb9IYzPVclWU1UaVdLipoaEBG40YDQSlAR1wuYrzDlGQRZuPUmWb937+1r3L0/fnuQM9+NXhdhu4fWjfJHWoAQtwocwrtyQHC6vX7u1WvctDBkmrb7YBbstTErhVQl1dHb82asRoIDQNAHA8j+frIi+0ksLLzRe5wq/iGaMlQrq7oqLCw6yHdyizdypp4JwFd1MDzz4noyjRbD9OrMpaD8vtlqFAAO4uLy8X8qNZL3OsJNEAf6uGLfg5wKPdJMBdXuMCv4plHF/cElWX06dPV5vREqUK89NJDZwDPPouCv4tTpVTtV5tgVvRvnv37hoDt64eE++MBgB4Bs+hDGvqvaA8pIlIXPIhXdIkjx7KNskvIa4q9xyyA24Py40ViHvlypXVPNbNMzobMRoITwMW4F7FeMOJzZKmxwVa71DPg7hIczNVvba+SdwSJFszTgntbh4xKTHWW1RmwnA0AMDTYcG5kK5ccAXLIyUlfBiLY9Rb3BLEscFVW1tbghUjRgOR0IAFuOaKRKJc7zJqG2wlnObiRTwRBbfQjg0uHg781Fhu1oSRiGkAgKfxuJy3ixGpdVS0qtb9KQeKYQ4V02K5BXA3j5gUI7MRo4FIakAAj2SZelmnKuggr1scYxvg1hNaecTkoLHcUI2RSGtAAI+UxZZyUM89h1wwyviamcWzWG5sVyZ94cKFn/L3CxE3YjQQcQ0AcCfPjAMwIyVMq2vhasenXJ5iWMoVuIV2165duxp4xOSYZDCh0UCkNaADLtY3nLC+yXZs14EmfsPTuqAEz9ZoiQU3p7XW1NQY1wTaMdJlGsC7urDg4Qpc6JoGN/xtuCSw3MKyB9xi0l0nTpzYbPzucNVu9g+kAQE8lDFwlOnh0TDKJ8tsmznZ4pfjHpYb+yABGVq3bNmywbjdUImRrtYAAHeE4IML2HJC4OvIW/e3buB66pZbVbudz41MP/3pT3fziwvmNnxX96wpX2kAgHd2ZtnGFqr68QuO3eCWF59uCQ5iWe7q6uqms2fPbjWuCdRiJBoaEMBDORb4LKugLYwrvmGuw62KEcuNFQtujrccO3bsAwO30pH5iZIGBPBgR05QrWNltk0c4K33gHADcGRq2bBhw3rjd7MmjERVAwAccAcj8LfX7Wxdz3kFbvCLRYleDOIYnOEX9tWca93OnDmznmd+7aFypuiP48jjhMVIdDUQjNdQVecuK7iheSLXrJIXPO7KM6ko46wAl9fMOE0Rj0Q1YsJhC8O9mT+Vfb09Ub8EhFaFKc2taVReYRmDMEszu4eigY4Ad7HZPnyKMAQoVtvjYhLH0eHGusCNHVr27du3euDAgRGF21axm2zH1pLN3Yzjxb3YG89Qel02PzIM3bUXf9jjtSdf4i8def3s4veFW39l+StHHaODjaGW5zd/R8fw08pQy2ppddOuva2rGVPFKrdN4EYzlehuCRKwDuB5Pk/KGzlyZM/169e/z5/vi8gXFwC28x9fZrDh1ieOVNW7cBcscSqcAjWtbXRXf/l/K6/ed6rpDDcXr5fh9jtAtzrKl+XGRtDXzJa77siRI6tHjBhxUyRcE9vhV8lWe4SLTizJR3XZLtRiwMlIzDWAx/qOn6HV+04Rf1iQ4AKAV3Brgc1x6/Y74iIw71hwFjTxqMlKniBTtoUV2lyJS0c+f+GLJzc3EgcaYI+EthyilVwVAAVOhVmP2unj3LJBLLfyZe6///5NVVVVRzty7mXHZA8BeC6PJcF3M0tsdADbzF94OPq9P5OMb4NTsdweCPqDWwCHyW8uKSl5zcB9Tm95fDWSg8FSIzHRAC48SyvoNT64YpNDARvMeogvuJEBZh474axo5ikf/trM784bOacBATzYO2kmH1t5/quLxNLCCK/aSX8Fl7yI1Qav7cQf3DgLLL/7ySefLC4rK9turPd5/QHwbOODn1dIFGKw2uW1tP3/3qZiPpzub7ez2qhOMHDj7GjasWPHSy1qSBG7GYEGlAVnwI3/HR0dtLK53XWCXmLVC9hgE0Y4JLg5v9oBO8L8N82cOfM1nvah1FhvqOa85BoLfl4ZXRiD1a6sp9L7lil/G3CDS79goyr+LDe24Wyw/G5+9axh+/btf4jUsCAOkCwigEfCpzRl+PbN8ZDUzhP0h5oadbNG97d9Wm2wFQhuAVxZ729+85t/raysPGmsd/vTEoBn8dRhRiKvAWW1G+jk7OXqQlKstt9REqlBR3AjD8w+CgHcjUePHq3duXPnImO9WRs+xFhw31Y33H8jWO29J2jR0Qr1QSc8+QcewSX49CuB4IblRgHqopLDxm9/+9sr+E2dMmO9fesUY+DGgvvWTWdSYbVrmqjsxytpBfjjRS4mO/S3caxAcCOPWG8FOD9vUrVnz54XjfWGanyLAG5GUcIfRcFzJPtP0ov7jhPe6RWwA1pt9EwwcIv1Vn4379M4Z86cl9h6VxrrDRX6FgW4GQf3rZwgU2G1qxup8ud/V8N/YrXBYUCrjUMEAzfyifVWvvfGjRvLN2/e/LS5awnV+Bfc5MnCOHiE7s6lWjktTN22Unp6awmVs5aD9rWlR4KF29t6N8yYMWMFT96z07xnKar0HQLwTDOK4ls5HaTCHTlVTTvvWqR8bTyrLaMkQVltFB0s3MjrYb358yL1y5Ytm8vzm7iMewL1+BcB3PjgwfnguMPC85G4Xt5Gc3nShnrWbMhWG70RCtztrPfDDz/88f79+/9sLi79gy1bBHBZN6F/DeA2+4Ez9OdfvE4fc65OWW2UHgrcyC/WG38ROJsavv/97/+Gb8ufNdYb6ulY4H/DRUk13zmU9kKDlY109sd/pd9wFGDLhWRQIyTYXyRUuGG9cRAMC+Kg9e+9914ZX2D+mt0UXjUSSAMAPMP75b5AO6XQ9iama0sJ/XrjQSrjZotLAt7AHfgLWkKFGwUL4GrkhNfr+eJyJd+93Gbck+D0LoCHYtFSIS/uRJ6oom23v6BeIROwwVnIYKMnOgs33BPrriXHG+bNm/cIv45WY0ZPoNbAIoAHznk+By5IIXJhKnGV2JYuaXpe2R4o1PfR4/720/PocX/5O0rH6EhlA9U88096hPPp7gg4A28hWW0cqzNwYz/xvS3r/dxzzx1cvXr1L3j0hOfZCLkeKDPlBP43XBSAEcwCBQlEelz21dP0uGwPFOr76HF/++l59Li//P7S20ZHaO0++sXv31cfbvK22uAtZOks3DgQDijWG2da/V133fXm3r17l5ubO1BPcALA01PcB29mp4OnaVj+jcX0JmsNYMsIiVjt4JTplSscuGGeAbhYb1So7pZbbvnV8ePH9xr/20vTHaxaFpxNWyr41nob4WefrKG9dy6kX4EfXsQlAVfgq9NuQAS+SsKHPy829rt5gquWDydMmPCVjIyMdMzaKWI/8S5hMdJeA+r7MNyN6GwR0Zy/v/NIp+O4ckw9LsfR0/S4bA81xAx1VQ1U+8s19K1/7qVjXGYtL7Dc8oCUpg0cMTQJx3LjSDi4t3tSN3/+fPjfjxn/O7TOyGAXJQ2f0ODdsEAkPLfWtb9yLH/HD7Q9lNrhsqyBnY639tJjv39X+dlitQXssKw26hIu3ChDABf3BGdeHfvfq/jR2BVm/BsqCl4E8OD3SMycGM/ec4JW3LuYVnELADa4wb2TsN0RLkNJJOBGQTjLMBbpAfj06dOfLC4uft8ADhUFLwAcF5m6b5pMcVxAlpyl9298jp5krXiDDY7AU9gSKbhREVhwffSkjt+3rLn55pt/WFpa+rEZQQmtrwA3XJRkE4B9pJw+/trz9MPKOjVhvLc7Ao4iIpGGW/xv/L2o0ZMDBw5U8HyDD/HjscUteEDXSNAaEMCTxWrjgaiT1VT84Ev00IFTVMGKELDBiwz7RQzurrYNqqKHDh1q4tvzGyZfmHVNbsUHufz5byNBasDRZn7kvpiuOokj9LXgEJJHj/vKK/kk7Ex+7KOLlIUQdyBP19CJOa/Q/W/soSOchJERwB1RP5vLs6Qr4PZ55vHFZb2r1b1lXNHRa/muXKYB3OqDgBFfgOvg+CtAz6PHuyo/jqEvOA7WYbF5GrSKX71NDyzaQAc4SYb88O+O67SI+dlcliVdATcKB+CyyMFsH+w6WsnTAO+8pD9dx4CnGcBFNYFDAVwfBw+8V+xzAGyeKar+D+vpu798k7ZzjWCtxR3Rh/0iXtmugluvqECO0MbPD5zpmUs7RvamKQx4Rgp/S0rXUVBxAVwpkk1ivPviAPtsPVUv2Uizfvaq+jgToBarDbBhsdGcLpFowo0GqIas3kOnud0fjBlAk/nWc450Wpe0MMkKFV3BB9ddgHiLtzC27GOfmvcWPTj3DfqIuwFQY4ErolvshIab22KdnWiIasyGYqrkZwreu/ICmsivYBVIpyGzkY41IP92cpHZce7ob23icY/jVXToZ3+jB55fR59wDfCNSN1iR3xkxFcro2G55bhyhlqAf3yEanccp7WfG05j+XszRQAcf7VGAmsgHgHHyQawedqzXQ8up/949WNrVEQHO2J3IANpKZpwS10EbhV+eoYaV++lt68dRaPyM2mAAVzUFDgUwJEz1v436oBb6p+W0cZbX6CHNn1KpzkJUMPP1h+GYo/U+ifnaNdJtOHWwZZGus/UUMufPqS114+mfvkZNNxpLHjQPS6Ax9JFwRh2Hdtjnqzy9WnP0k9Ky9QNmpiCDQVGG27pNB1yxF31jeT63Xu07ooLqLx3N7oy3UFO6TjZyYS+NaAPqUbbgquhvkZq5FGwX13/ND3L/YgPnsLH9jXch76OmsQKbjRQQd0WWrAv30r7bXbaOLIPXZHppG7GTQmOBQ/AeZeuHj1B7/HEOXSikg4/vZZmzV5Ba/mwsNbiX8uoiNygQR9HVWIJtzQUjYaLYrkp6w5QOfvhb04aSn3YDx9m3BRRVcehAN7VFCk3hAfz9p6kN29fQD9Y+REd4poJ2GKx9TuPXV0ln4qJNdxotPeiQD9dTc3sprw39gI60zufxsFNkb9cny0xiUoDMtrUFTTBr1e30huo8e399D/TfkPPcj9V8oF1/1qeFRGLHbOeiTXcaLj0g1hvPXSv2EqfuG20bngRXc43fAod/H8rHRgzrcX5gS39tOlKjEI4IcDGmzPHKql43ts0i7/g+y6rQe44yoiIgC19GFNNwTWLF0FdcLJh4cf1Cd/p5Q9SqyW7ex7lLL2X7vjsILq3WyZ/vIBzWZ3ImYy01wDch3AFUOMZbJ5TpH7rYVrAs64uPVvtYan1N2hgrbGIwQr38GHtHw+WW28AlCKLnP0IW+ubyLX0A9pxqIzWXNib+vLk7oPlYtNArqvwfDwcvYgLUsu2eP9pemfOSvrBwyvpHe4HuCAyGgKwvZ/siwuwoYV4styojwieYsaCGT14dj1lxfl7YZYlz3j2drr6Xy6m2T3zqD+PqpAZNmTt+BGAiiVYgcWHC8L3H46+toOeenAZvc/7wuUAzAI01vVnRCLwP8ElRlDiFW40EXUTwAVyAG4tA3tR3oI76J4xA+mOvHTKgKtiIIfq2kswcANquCDVTdTIj0YsvXcpLSo9rcatYZ31RaDmU8Aa5Wp/0BinxDPcUA3qhwXuEwCHLw5LbgGO+MyraNCDk+nuEb1pOrsr6TyyYiBnxXiLP8ABNW6dswvStP8UrXrmPVr8x410mPfXgUYcUGOID1CLbx3CfwLvFUWJd7hFFagnrLhALq4KLjoF9IyvXkb9Zk+lmSOK6Cv8XfZMfl7cQC4a9BECatyIqW6gBob6b0+toT/+5SM1OQ5cDgEbcd0FAdRwQeIWaq6bkkSBG5VFXQVyWHFxVQC4QK7iU4ZTr4e/RLeN7kdfzcugXAU57xnOBRYfIykE1htv8yioG6lm1zH6y3/9nZat/UQ96CQgA2yJ+3JB4h5sdFYiwS1wCeDe/jjAFosOa57+2c9Q4X9/mW4Z3Zdm8J3OQszJJ3c7Uwl0AI0Fkw80sFPBU5iV7zpOL//0VVqx9VP1pTAALEAjrltq8asTwlpz3S1JRLil8gI5XBUs8MdlfFwgV8AX5lHW4zfSxCsG0vUDu9NEfnY8Xax5Ml+Awu0QK13bRE2lZ2n9llJ6g4f11pdXW4+h6hYacfjUcus8YVwQrnM7SWS40RjUXyAXn1wgB+ACucTTxw2mbj+cRlNH9aZp/QroYobcpmZ3QkFcUiJbdLHQ8Bnw0gC7Hu5jFbRzDz8Dwi/nrtlUom6VwzLLIhYa6zrUsNJiqRPCBeH6tpNEh1sa5AtyfXQFcAN6gVydAHdNoAF3j6PpQ3rQlIIcGoxRFoCubg5x5niHXYcZz3wAaIx6VNRSSXEZrV28iVYt2UBHuCkCrkCNdT0O10OsdMJDzW1Rkixw6+0R0MVd0V0WuQgV0MXKO798CRV9bRxdMbQHje3TjS7n0Za+cF0wdq7DjgPFwroDZIgCmkPAjDHpttGO4/zo6YcHy2jbnzbRlld30CnOAmB1qAVoPR1Ay4IjyMLRxJdkg1vvEbQNroq4KwI7ABeovUNsQz7nzHHU78YxdMWQnjSWn0q8LDuNemIObVyQAnbrwpQzQ3Tg9fi5rYF/BV7klLgijX9wIQiYEeKtcn7r5czJKvqo+AxtW7mdtvxxkxq+E+urwytwSyh5BGgu0XI/AlcywXIkM9zSFWLJBXSEFsQcB+BYF+glriBvS7dPGU6FUy+iwcOKaFBRPg3qlkUD89NpIFv4fmzdnQAez1OLK6POLK91bBPLq0IGFxd8AjHSsN5mlVt4/PlYVROV8qQ2paeq6DDPr3d4zW4q4WE7fAsdYAJWAVbiANk7DpiRJjAjVIflMGkFfZAqgrbqC+AF6Ahl8QU20mS7vo86WXIzyfm1K6jfZQNpQEEm5fP0w1lZaZTNw47Z7L9ns4XPZl8+i61+NsOdwQA3svWtY9+4ni1xHfvJdTw8V1fPS2Mz1Vc0UNVHpXTkT1voWE2DB5AAFFCK1RVgBWR9Xc8j+wjMEnJRyS2pBLfekzrkiCtQOdThFaB9wS0nhYRShne5so5jIy4CwCACmncolhWQCpwIJS7wAmyJ+8rrXS5nTx3RFZ46rfZsqQCohwK7Hgr4HaXpZUgcR9PjWBfo9LikIRSQBWZ93V8a0vUyJI5jpKRA6UY8NSAgeoeAGmmBQu/9UDrSvAXwQQRCPRRQA4X6PhI/V6r59al0oxZPDQisSPUVlzQ9lLx6iLi3AEiIHgqk3qHk886rCjA/7TWADjESugZ0vUncO5RSJV3W9VBAlTRZ9w6xXdIkrwkDaKAjxQfY1WwOoIFQdGvADaDMzmz+f6SMYEX4z7hMAAAAAElFTkSuQmCC' // @suppress longLineCheck
+ };
+
+ return {
+ FaviconsByHue,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/file.html b/chromium/third_party/catapult/tracing/tracing/ui/base/file.html
new file mode 100644
index 00000000000..0c4945933f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/file.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function readFile(fileBlob) {
+ return new Promise(function(resolve, reject) {
+ const reader = new FileReader();
+ const filename = fileBlob.name;
+ reader.onload = function(data) {
+ resolve(data.target.result);
+ };
+ reader.onerror = function(err) {
+ reject(err);
+ };
+
+ const isBinary = filename.endsWith('.gz') || filename.endsWith('.zip');
+ if (isBinary) {
+ reader.readAsArrayBuffer(fileBlob);
+ } else {
+ reader.readAsText(fileBlob);
+ }
+ });
+ }
+ return {
+ readFile,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html
new file mode 100644
index 00000000000..942d83f9542
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-b-grouping-table'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ #table {
+ flex: 1 1 auto;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function Row(title, data, groupingKeyFuncs, rowStatsConstructor) {
+ this.title = title;
+ this.data_ = data;
+ if (groupingKeyFuncs === undefined) {
+ groupingKeyFuncs = [];
+ }
+ this.groupingKeyFuncs_ = groupingKeyFuncs;
+ this.rowStatsConstructor_ = rowStatsConstructor;
+
+ this.subRowsBuilt_ = false;
+ this.subRows_ = undefined;
+
+ this.rowStats_ = undefined;
+ }
+
+ Row.prototype = {
+ getCurrentGroupingKeyFunc_() {
+ if (this.groupingKeyFuncs_.length === 0) return undefined;
+ return this.groupingKeyFuncs_[0];
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ get rowStats() {
+ if (this.rowStats_ === undefined) {
+ this.rowStats_ = new this.rowStatsConstructor_(this);
+ }
+ return this.rowStats_;
+ },
+
+ rebuildSubRowsIfNeeded_() {
+ if (this.subRowsBuilt_) return;
+ this.subRowsBuilt_ = true;
+
+ const groupingKeyFunc = this.getCurrentGroupingKeyFunc_();
+ if (groupingKeyFunc === undefined) {
+ this.subRows_ = undefined;
+ return;
+ }
+
+ const dataByKey = {};
+ let hasValues = false;
+ this.data_.forEach(function(datum) {
+ const key = groupingKeyFunc(datum);
+ hasValues = hasValues || (key !== undefined);
+ if (dataByKey[key] === undefined) {
+ dataByKey[key] = [];
+ }
+ dataByKey[key].push(datum);
+ });
+ if (!hasValues) {
+ this.subRows_ = undefined;
+ return;
+ }
+
+ this.subRows_ = [];
+ for (const key in dataByKey) {
+ const row = new Row(key,
+ dataByKey[key],
+ this.groupingKeyFuncs_.slice(1),
+ this.rowStatsConstructor_);
+ this.subRows_.push(row);
+ }
+ },
+
+ get isExpanded() {
+ return (this.subRows &&
+ (this.subRows.length > 0) &&
+ (this.subRows.length < 5));
+ },
+
+ get subRows() {
+ this.rebuildSubRowsIfNeeded_();
+ return this.subRows_;
+ }
+ };
+
+ Polymer({
+ is: 'tr-ui-b-grouping-table',
+
+ created() {
+ this.dataToGroup_ = undefined;
+ this.groupBy_ = undefined;
+ this.rowStatsConstructor_ = undefined;
+ },
+
+ get tableColumns() {
+ return this.$.table.tableColumns;
+ },
+
+ set tableColumns(tableColumns) {
+ this.$.table.tableColumns = tableColumns;
+ },
+
+ get tableRows() {
+ return this.$.table.tableRows;
+ },
+
+ get sortColumnIndex() {
+ return this.$.table.sortColumnIndex;
+ },
+
+ set sortColumnIndex(sortColumnIndex) {
+ this.$.table.sortColumnIndex = sortColumnIndex;
+ },
+
+ get sortDescending() {
+ return this.$.table.sortDescending;
+ },
+
+ set sortDescending(sortDescending) {
+ this.$.table.sortDescending = sortDescending;
+ },
+
+ get selectionMode() {
+ return this.$.table.selectionMode;
+ },
+
+ set selectionMode(selectionMode) {
+ this.$.table.selectionMode = selectionMode;
+ },
+
+ get rowHighlightStyle() {
+ return this.$.table.rowHighlightStyle;
+ },
+
+ set rowHighlightStyle(rowHighlightStyle) {
+ this.$.table.rowHighlightStyle = rowHighlightStyle;
+ },
+
+ get cellHighlightStyle() {
+ return this.$.table.cellHighlightStyle;
+ },
+
+ set cellHighlightStyle(cellHighlightStyle) {
+ this.$.table.cellHighlightStyle = cellHighlightStyle;
+ },
+
+ get selectedColumnIndex() {
+ return this.$.table.selectedColumnIndex;
+ },
+
+ set selectedColumnIndex(selectedColumnIndex) {
+ this.$.table.selectedColumnIndex = selectedColumnIndex;
+ },
+
+ get selectedTableRow() {
+ return this.$.table.selectedTableRow;
+ },
+
+ set selectedTableRow(selectedTableRow) {
+ this.$.table.selectedTableRow = selectedTableRow;
+ },
+
+ get groupBy() {
+ return this.groupBy_;
+ },
+
+ set groupBy(groupBy) {
+ this.groupBy_ = groupBy;
+ this.updateContents_();
+ },
+
+ get dataToGroup() {
+ return this.dataToGroup_;
+ },
+
+ set dataToGroup(dataToGroup) {
+ this.dataToGroup_ = dataToGroup;
+ this.updateContents_();
+ },
+
+ get rowStatsConstructor() {
+ return this.rowStatsConstructor_;
+ },
+
+ set rowStatsConstructor(rowStatsConstructor) {
+ this.rowStatsConstructor_ = rowStatsConstructor;
+ this.updateContents_();
+ },
+
+ rebuild() {
+ this.$.table.rebuild();
+ },
+
+ updateContents_() {
+ const groupBy = this.groupBy_ || [];
+ const dataToGroup = this.dataToGroup_ || [];
+ const rowStatsConstructor = this.rowStatsConstructor_ || function() {};
+
+ const superRow = new Row('', dataToGroup, groupBy,
+ rowStatsConstructor);
+ this.$.table.tableRows = superRow.subRows || [];
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html
new file mode 100644
index 00000000000..6d2f1b917e6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker.html
@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/dropdown.html">
+
+<dom-module id='tr-ui-b-grouping-table-groupby-picker'>
+ <template>
+ <style>
+ #container {
+ display: flex;
+ }
+ #container *:not(:first-child) {
+ padding-left: 3px;
+ border-left: 1px solid black;
+ margin-left: 3px;
+ }
+ </style>
+
+ <div id="container"></div>
+ </template>
+</dom-module>
+
+<dom-module id="tr-ui-b-grouping-table-groupby-picker-group">
+ <template>
+ <style>
+ :host {
+ white-space: nowrap;
+ }
+ #left, #right {
+ user-select: none;
+ cursor: pointer;
+ }
+ </style>
+
+ <span id="left" on-click="moveLeft_">&#9664;</span>
+ <input type="checkbox" id="enabled" on-change="onEnableChanged_">
+ <label for="enabled" id="label"></label>
+ <span id="right" on-click="moveRight_">&#9654;</span>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ Polymer({
+ is: 'tr-ui-b-grouping-table-groupby-picker-group',
+
+ created() {
+ this.picker_ = undefined;
+ this.group_ = undefined;
+ },
+
+ get picker() {
+ return this.picker_;
+ },
+
+ set picker(picker) {
+ this.picker_ = picker;
+ },
+
+ get group() {
+ return this.group_;
+ },
+
+ set group(g) {
+ this.group_ = g;
+ this.$.label.textContent = g.label;
+ },
+
+ get enabled() {
+ return this.$.enabled.checked;
+ },
+
+ set enabled(enabled) {
+ this.$.enabled.checked = enabled;
+ if (!this.enabled) {
+ this.$.left.style.display = 'none';
+ this.$.right.style.display = 'none';
+ }
+ },
+
+ set isFirst(isFirst) {
+ this.$.left.style.display = (!this.enabled || isFirst) ? 'none' :
+ 'inline';
+ },
+
+ set isLast(isLast) {
+ this.$.right.style.display = (!this.enabled || isLast) ? 'none' :
+ 'inline';
+ },
+
+ moveLeft_() {
+ this.picker.moveLeft_(this);
+ },
+
+ moveRight_() {
+ this.picker.moveRight_(this);
+ },
+
+ onEnableChanged_() {
+ if (!this.enabled) {
+ this.$.left.style.display = 'none';
+ this.$.right.style.display = 'none';
+ }
+ this.picker.onEnableChanged_(this);
+ }
+ });
+
+ Polymer({
+ is: 'tr-ui-b-grouping-table-groupby-picker',
+
+ created() {
+ this.settingsKey_ = undefined;
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ this.settingsKey_ = settingsKey;
+ if (this.$.container.children.length) {
+ this.restoreSetting_();
+ }
+ },
+
+ restoreSetting_() {
+ if (this.settingsKey_ === undefined) return;
+ this.currentGroupKeys = tr.b.Settings.get(this.settingsKey_,
+ this.currentGroupKeys);
+ },
+
+ get possibleGroups() {
+ return [...this.$.container.children].map(groupEl => groupEl.group);
+ },
+
+ set possibleGroups(possibleGroups) {
+ Polymer.dom(this.$.container).textContent = '';
+ for (let i = 0; i < possibleGroups.length; ++i) {
+ const groupEl = document.createElement(
+ 'tr-ui-b-grouping-table-groupby-picker-group');
+ groupEl.picker = this;
+ groupEl.group = possibleGroups[i];
+ Polymer.dom(this.$.container).appendChild(groupEl);
+ }
+ this.restoreSetting_();
+ this.updateFirstLast_();
+ },
+
+ updateFirstLast_() {
+ const groupEls = this.$.container.children;
+ const enabledGroupEls = [...groupEls].filter(el => el.enabled);
+ for (let i = 0; i < enabledGroupEls.length; ++i) {
+ enabledGroupEls[i].isFirst = i === 0;
+ enabledGroupEls[i].isLast = i === enabledGroupEls.length - 1;
+ }
+ },
+
+ get currentGroupKeys() {
+ return this.currentGroups.map(group => group.key);
+ },
+
+ get currentGroups() {
+ const groups = [];
+ for (const groupEl of this.$.container.children) {
+ if (groupEl.enabled) {
+ groups.push(groupEl.group);
+ }
+ }
+ return groups;
+ },
+
+ set currentGroupKeys(newKeys) {
+ if (!tr.b.compareArrays(this.currentGroupKeys, newKeys,
+ (x, y) => x.localeCompare(y))) {
+ return;
+ }
+
+ const possibleGroups = new Map();
+ for (const group of this.possibleGroups) {
+ possibleGroups.set(group.key, group);
+ }
+
+ const groupEls = this.$.container.children;
+
+ let i = 0;
+ for (i = 0; i < newKeys.length; ++i) {
+ const group = possibleGroups.get(newKeys[i]);
+ if (group === undefined) {
+ newKeys.splice(i, 1);
+ --i;
+ continue;
+ }
+ groupEls[i].group = group;
+ groupEls[i].enabled = true;
+ possibleGroups.delete(newKeys[i]);
+ }
+
+ for (const group of possibleGroups.values()) {
+ groupEls[i].group = group;
+ groupEls[i].enabled = false;
+ ++i;
+ }
+
+ this.updateFirstLast_();
+ this.onCurrentGroupsChanged_();
+ },
+
+ moveLeft_(groupEl) {
+ const reference = groupEl.previousSibling;
+ Polymer.dom(this.$.container).removeChild(groupEl);
+ Polymer.dom(this.$.container).insertBefore(groupEl, reference);
+ this.updateFirstLast_();
+
+ if (groupEl.enabled) {
+ this.onCurrentGroupsChanged_();
+ }
+ },
+
+ moveRight_(groupEl) {
+ const reference = groupEl.nextSibling.nextSibling;
+ Polymer.dom(this.$.container).removeChild(groupEl);
+ if (reference) {
+ Polymer.dom(this.$.container).insertBefore(groupEl, reference);
+ } else {
+ Polymer.dom(this.$.container).appendChild(groupEl);
+ }
+ this.updateFirstLast_();
+
+ if (groupEl.enabled) {
+ this.onCurrentGroupsChanged_();
+ }
+ },
+
+ onCurrentGroupsChanged_() {
+ this.dispatchEvent(new tr.b.Event('current-groups-changed'));
+ tr.b.Settings.set(this.settingsKey_, this.currentGroupKeys);
+ },
+
+ onEnableChanged_(groupEl) {
+ this.updateFirstLast_();
+ this.onCurrentGroupsChanged_();
+ }
+ });
+
+ return {
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html
new file mode 100644
index 00000000000..727d430224f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/grouping_table_groupby_picker_test.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('groupby-picker', function() {
+ const settingsKey = 'tr-ui-b-grouping-table-groupby-picker-test';
+ const picker = document.createElement(
+ 'tr-ui-b-grouping-table-groupby-picker');
+ tr.b.Settings.set(settingsKey, []);
+ picker.settingsKey = settingsKey;
+ picker.possibleGroups = [
+ {key: 'a', label: 'A'},
+ {key: 'b', label: 'B'},
+ {key: 'c', label: 'C'},
+ {key: 'd', label: 'D'},
+ {key: 'e', label: 'E'}
+ ];
+ assert.deepEqual([], picker.currentGroupKeys);
+ this.addHTMLOutput(picker);
+
+ let keys = ['a', 'b', 'c', 'd', 'e'];
+ picker.currentGroupKeys = keys;
+ assert.deepEqual(keys, picker.currentGroupKeys);
+
+ keys = ['e', 'd', 'c', 'b', 'a'];
+ picker.currentGroupKeys = keys;
+ assert.deepEqual(keys, picker.currentGroupKeys);
+
+ keys = [];
+ picker.currentGroupKeys = keys;
+ assert.deepEqual(keys, picker.currentGroupKeys);
+
+ keys = ['a', 'b', 'd'];
+ picker.currentGroupKeys = keys;
+ assert.deepEqual(keys, picker.currentGroupKeys);
+
+ tr.b.Settings.set(settingsKey, ['foo']);
+ picker.settingsKey = settingsKey;
+ assert.deepEqual([], picker.currentGroupKeys);
+
+ tr.b.Settings.set(settingsKey, ['e']);
+ picker.settingsKey = settingsKey;
+ assert.deepEqual(['e'], picker.currentGroupKeys);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html b/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html
new file mode 100644
index 00000000000..9b625c61b03
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/heading.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel='import' href='/tracing/ui/base/constants.html'>
+
+<dom-module id='tr-ui-b-heading'>
+ <template>
+ <style>
+ :host {
+ background-color: rgb(243, 245, 247);
+ border-right: 1px solid #8e8e8e;
+ display: block;
+ height: 100%;
+ margin: 0;
+ padding: 0 5px 0 0;
+ }
+
+ heading {
+ display: block;
+ overflow-x: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ #arrow {
+ flex: 0 0 auto;
+ font-family: sans-serif;
+ margin-left: 5px;
+ margin-right: 5px;
+ width: 8px;
+ }
+
+ #link, #heading_content {
+ display: none;
+ }
+ </style>
+ <heading id='heading' on-click='onHeadingDivClicked_'>
+ <span id='arrow'></span>
+ <span id='heading_content'></span>
+ <tr-ui-a-analysis-link id='link'></tr-ui-a-analysis-link>
+ </heading>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-b-heading',
+
+ DOWN_ARROW: String.fromCharCode(0x25BE),
+ RIGHT_ARROW: String.fromCharCode(0x25B8),
+
+ ready(viewport) {
+ // Minus 6 === 1px border + 5px padding right.
+ this.style.width = (tr.ui.b.constants.HEADING_WIDTH - 6) + 'px';
+
+ this.heading_ = '';
+ this.expanded_ = true;
+ this.arrowVisible_ = false;
+ this.selectionGenerator_ = undefined;
+
+ this.updateContents_();
+ },
+
+ get heading() {
+ return this.heading_;
+ },
+
+ set heading(text) {
+ if (this.heading_ === text) return;
+
+ this.heading_ = text;
+ this.updateContents_();
+ },
+
+ set arrowVisible(val) {
+ if (this.arrowVisible_ === val) return;
+
+ this.arrowVisible_ = !!val;
+ this.updateContents_();
+ },
+
+ set tooltip(text) {
+ this.$.heading.title = text;
+ },
+
+ set selectionGenerator(generator) {
+ if (this.selectionGenerator_ === generator) return;
+
+ this.selectionGenerator_ = generator;
+ this.updateContents_();
+ },
+
+ get expanded() {
+ return this.expanded_;
+ },
+
+ set expanded(expanded) {
+ if (this.expanded_ === expanded) return;
+
+ this.expanded_ = !!expanded;
+ this.updateContents_();
+ },
+
+ onHeadingDivClicked_() {
+ this.dispatchEvent(new tr.b.Event('heading-clicked', true));
+ },
+
+ updateContents_() {
+ if (this.arrowVisible_) {
+ this.$.arrow.style.display = '';
+ } else {
+ this.$.arrow.style.display = 'none';
+ this.$.heading.style.display = this.expanded_ ? '' : 'none';
+ }
+
+ if (this.arrowVisible_) {
+ Polymer.dom(this.$.arrow).textContent =
+ this.expanded_ ? this.DOWN_ARROW : this.RIGHT_ARROW;
+ }
+
+ this.$.link.style.display = 'none';
+ this.$.heading_content.style.display = 'none';
+
+ if (this.selectionGenerator_) {
+ this.$.link.style.display = 'inline-block';
+ this.$.link.selection = this.selectionGenerator_;
+ Polymer.dom(this.$.link).textContent = this.heading_;
+ } else {
+ this.$.heading_content.style.display = 'inline-block';
+ Polymer.dom(this.$.heading_content).textContent = this.heading_;
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html
new file mode 100644
index 00000000000..3ffd96bfbe3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hot_key.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/guid.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function HotKey(dict) {
+ if (dict.eventType === undefined) {
+ throw new Error('eventType must be given');
+ }
+ if (dict.keyCode === undefined && dict.keyCodes === undefined) {
+ throw new Error('keyCode or keyCodes must be given');
+ }
+ if (dict.keyCode !== undefined && dict.keyCodes !== undefined) {
+ throw new Error('Only keyCode or keyCodes can be given');
+ }
+ if (dict.callback === undefined) {
+ throw new Error('callback must be given');
+ }
+
+ this.eventType_ = dict.eventType;
+ this.keyCodes_ = [];
+
+ if (dict.keyCode) {
+ this.pushKeyCode_(dict.keyCode);
+ } else if (dict.keyCodes) {
+ dict.keyCodes.forEach(this.pushKeyCode_, this);
+ }
+
+ this.useCapture_ = !!dict.useCapture;
+ this.callback_ = dict.callback;
+ this.thisArg_ = dict.thisArg !== undefined ? dict.thisArg : undefined;
+
+ this.helpText_ = dict.helpText !== undefined ? dict.helpText : undefined;
+ }
+
+ HotKey.prototype = {
+ get eventType() {
+ return this.eventType_;
+ },
+
+ get keyCodes() {
+ return this.keyCodes_;
+ },
+
+ get helpText() {
+ return this.helpText_;
+ },
+
+ call(e) {
+ this.callback_.call(this.thisArg_, e);
+ },
+
+ pushKeyCode_(keyCode) {
+ this.keyCodes_.push(keyCode);
+ }
+ };
+
+ return {
+ HotKey,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html
new file mode 100644
index 00000000000..f56631dc350
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller.html
@@ -0,0 +1,310 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/ui/base/hot_key.html">
+
+<dom-module id='tv-ui-b-hotkey-controller'>
+ <template>
+ <div></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tv-ui-b-hotkey-controller',
+
+ created() {
+ this.isAttached_ = false;
+ this.globalMode_ = false;
+ this.slavedToParentController_ = undefined;
+ this.curHost_ = undefined;
+ this.childControllers_ = [];
+
+ this.bubblingKeyDownHotKeys_ = {};
+ this.capturingKeyDownHotKeys_ = {};
+ this.bubblingKeyPressHotKeys_ = {};
+ this.capturingKeyPressHotKeys_ = {};
+
+ this.onBubblingKeyDown_ = this.onKey_.bind(this, false);
+ this.onCapturingKeyDown_ = this.onKey_.bind(this, true);
+ this.onBubblingKeyPress_ = this.onKey_.bind(this, false);
+ this.onCapturingKeyPress_ = this.onKey_.bind(this, true);
+ },
+
+ attached() {
+ this.isAttached_ = true;
+
+ const host = this.findHost_();
+ if (host.__hotkeyController) {
+ throw new Error('Multiple hotkey controllers attached to this host');
+ }
+
+ host.__hotkeyController = this;
+ this.curHost_ = host;
+
+ let parentElement;
+ if (host.parentElement) {
+ parentElement = host.parentElement;
+ } else {
+ parentElement = Polymer.dom(host).parentNode.host;
+ }
+ const parentController = tr.b.getHotkeyControllerForElement(
+ parentElement);
+
+ if (parentController) {
+ this.slavedToParentController_ = parentController;
+ parentController.addChildController_(this);
+ return;
+ }
+
+ host.addEventListener('keydown', this.onBubblingKeyDown_, false);
+ host.addEventListener('keydown', this.onCapturingKeyDown_, true);
+ host.addEventListener('keypress', this.onBubblingKeyPress_, false);
+ host.addEventListener('keypress', this.onCapturingKeyPress_, true);
+ },
+
+ detached() {
+ this.isAttached_ = false;
+
+ const host = this.curHost_;
+ if (!host) return;
+
+ delete host.__hotkeyController;
+ this.curHost_ = undefined;
+
+ if (this.slavedToParentController_) {
+ this.slavedToParentController_.removeChildController_(this);
+ this.slavedToParentController_ = undefined;
+ return;
+ }
+
+ host.removeEventListener('keydown', this.onBubblingKeyDown_, false);
+ host.removeEventListener('keydown', this.onCapturingKeyDown_, true);
+ host.removeEventListener('keypress', this.onBubblingKeyPress_, false);
+ host.removeEventListener('keypress', this.onCapturingKeyPress_, true);
+ },
+
+ addChildController_(controller) {
+ const i = this.childControllers_.indexOf(controller);
+ if (i !== -1) {
+ throw new Error('Controller already registered');
+ }
+ this.childControllers_.push(controller);
+ },
+
+ removeChildController_(controller) {
+ const i = this.childControllers_.indexOf(controller);
+ if (i === -1) {
+ throw new Error('Controller not registered');
+ }
+ this.childControllers_.splice(i, 1);
+ return controller;
+ },
+
+ getKeyMapForEventType_(eventType, useCapture) {
+ if (eventType === 'keydown') {
+ if (!useCapture) {
+ return this.bubblingKeyDownHotKeys_;
+ }
+ return this.capturingKeyDownHotKeys_;
+ }
+ if (eventType === 'keypress') {
+ if (!useCapture) {
+ return this.bubblingKeyPressHotKeys_;
+ }
+ return this.capturingKeyPressHotKeys_;
+ }
+
+ throw new Error('Unsupported key event');
+ },
+
+ addHotKey(hotKey) {
+ if (!(hotKey instanceof tr.ui.b.HotKey)) {
+ throw new Error('hotKey must be a tr.ui.b.HotKey');
+ }
+
+ const keyMap = this.getKeyMapForEventType_(
+ hotKey.eventType, hotKey.useCapture);
+
+ for (let i = 0; i < hotKey.keyCodes.length; i++) {
+ const keyCode = hotKey.keyCodes[i];
+ if (keyMap[keyCode]) {
+ throw new Error('Key is already bound for keyCode=' + keyCode);
+ }
+ }
+
+ for (let i = 0; i < hotKey.keyCodes.length; i++) {
+ const keyCode = hotKey.keyCodes[i];
+ keyMap[keyCode] = hotKey;
+ }
+ return hotKey;
+ },
+
+ removeHotKey(hotKey) {
+ if (!(hotKey instanceof tr.ui.b.HotKey)) {
+ throw new Error('hotKey must be a tr.ui.b.HotKey');
+ }
+
+ const keyMap = this.getKeyMapForEventType_(
+ hotKey.eventType, hotKey.useCapture);
+
+ for (let i = 0; i < hotKey.keyCodes.length; i++) {
+ const keyCode = hotKey.keyCodes[i];
+ if (!keyMap[keyCode]) {
+ throw new Error('Key is not bound for keyCode=' + keyCode);
+ }
+ keyMap[keyCode] = hotKey;
+ }
+ for (let i = 0; i < hotKey.keyCodes.length; i++) {
+ const keyCode = hotKey.keyCodes[i];
+ delete keyMap[keyCode];
+ }
+ return hotKey;
+ },
+
+ get globalMode() {
+ return this.globalMode_;
+ },
+
+ set globalMode(globalMode) {
+ const wasAttached = this.isAttached_;
+ if (wasAttached) {
+ this.detached();
+ }
+ this.globalMode_ = !!globalMode;
+ if (wasAttached) {
+ this.attached();
+ }
+ },
+
+ get topmostConroller_() {
+ if (this.slavedToParentController_) {
+ return this.slavedToParentController_.topmostConroller_;
+ }
+ return this;
+ },
+
+ childRequestsGeneralFocus(child) {
+ const topmost = this.topmostConroller_;
+ if (topmost.curHost_) {
+ if (topmost.curHost_.hasAttribute('tabIndex')) {
+ topmost.curHost_.focus();
+ } else {
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+ }
+ } else {
+ if (document.activeElement) {
+ document.activeElement.blur();
+ }
+ }
+ },
+
+ childRequestsBlur(child) {
+ child.blur();
+
+ const topmost = this.topmostConroller_;
+ if (topmost.curHost_) {
+ topmost.curHost_.focus();
+ }
+ },
+
+ findHost_() {
+ if (this.globalMode_) return document.body;
+ if (this.parentElement) return this.parentElement;
+ if (!Polymer.dom(this).parentNode) return this.host;
+
+ let node = this.parentNode;
+ while (Polymer.dom(node).parentNode) node = Polymer.dom(node).parentNode;
+ return node.host;
+ },
+
+ appendMatchingHotKeysTo_(matchedHotKeys,
+ useCapture, e) {
+ const localKeyMap = this.getKeyMapForEventType_(e.type, useCapture);
+ const localHotKey = localKeyMap[e.keyCode];
+ if (localHotKey) {
+ matchedHotKeys.push(localHotKey);
+ }
+
+ for (let i = 0; i < this.childControllers_.length; i++) {
+ const controller = this.childControllers_[i];
+ controller.appendMatchingHotKeysTo_(matchedHotKeys,
+ useCapture, e);
+ }
+ },
+
+ onKey_(useCapture, e) {
+ // Keys dispatched to INPUT elements still bubble, even when they're
+ // handled. So, skip any events that targeted the input element.
+ if (!useCapture && e.path[0].tagName === 'INPUT') return;
+
+ let sortedControllers;
+
+ const matchedHotKeys = [];
+ this.appendMatchingHotKeysTo_(matchedHotKeys, useCapture, e);
+
+ if (matchedHotKeys.length === 0) return false;
+
+ if (matchedHotKeys.length > 1) {
+ // TODO(nduca): To do support for coddling hotKeys, we need to
+ // sort the listeners by their capturing/bubbling order and then pick
+ // the one that would topologically win the tie, per DOM dispatch rules.
+ throw new Error('More than one hotKey is currently unsupported');
+ }
+
+
+ const hotKey = matchedHotKeys[0];
+
+ let prevented = 0;
+ prevented |= hotKey.call(e);
+
+ // We want to return false if preventDefaulted, or one of the handlers
+ // return false. But otherwise, we want to return undefiend.
+ return !prevented && e.defaultPrevented;
+ }
+});
+</script>
+<script>
+'use strict';
+tr.exportTo('tr.b', function() {
+ function getHotkeyControllerForElement(refElement) {
+ let curElement = refElement;
+ while (curElement) {
+ if (curElement.tagName === 'tv-ui-b-hotkey-controller') {
+ return curElement;
+ }
+
+ if (curElement.__hotkeyController) {
+ return curElement.__hotkeyController;
+ }
+
+ if (curElement.parentElement) {
+ curElement = curElement.parentElement;
+ continue;
+ }
+
+ // Probably inside a shadow
+ curElement = findHost(curElement);
+ }
+ return undefined;
+ }
+
+ function findHost(initialNode) {
+ let node = initialNode;
+ while (Polymer.dom(node).parentNode) {
+ node = Polymer.dom(node).parentNode;
+ }
+ return node.host;
+ }
+
+ return {
+ getHotkeyControllerForElement,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html
new file mode 100644
index 00000000000..cff10cb775c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/hotkey_controller_test.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const KeyEventManager = tr.b.KeyEventManager;
+
+ function newKeyEvent(eventType, dict) {
+ const e = new tr.b.Event(eventType, true, true);
+ if (dict.keyCode === undefined) {
+ throw new Error('keyCode required');
+ }
+ e.keyCode = dict.keyCode;
+ return e;
+ }
+
+ test('simpleHotkeyManager', function() {
+ const rootElement = document.createElement('div');
+ Polymer.dom(document.body).appendChild(rootElement);
+ try {
+ const elementShadow = rootElement.createShadowRoot();
+
+ const hkc = document.createElement('tv-ui-b-hotkey-controller');
+ Polymer.dom(elementShadow).appendChild(hkc);
+
+ const subElement = document.createElement('div');
+ Polymer.dom(elementShadow).appendChild(subElement);
+
+ assert.strictEqual(tr.b.getHotkeyControllerForElement(subElement), hkc);
+
+ let didGetCalled = false;
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keydown',
+ keyCode: 73, useCapture: true,
+ callback() {
+ didGetCalled = true;
+ }
+ }));
+
+ // Ensure it is called when events target the root element.
+ let e = newKeyEvent('keydown', {keyCode: 73});
+ rootElement.dispatchEvent(e);
+ assert.isTrue(didGetCalled);
+
+ // Ensure it is still called when we target the sub element.
+ didGetCalled = false;
+ e = newKeyEvent('keydown', {keyCode: 73});
+ subElement.dispatchEvent(e);
+ assert.isTrue(didGetCalled);
+ } finally {
+ Polymer.dom(document.body).removeChild(rootElement);
+ }
+ });
+
+ test('nestedHotkeyController', function() {
+ const rootElement = document.createElement('div');
+ Polymer.dom(document.body).appendChild(rootElement);
+ try {
+ const elementShadow = rootElement.createShadowRoot();
+
+ const hkc = document.createElement('tv-ui-b-hotkey-controller');
+ Polymer.dom(elementShadow).appendChild(hkc);
+
+ const subElement = document.createElement('div');
+ Polymer.dom(elementShadow).appendChild(subElement);
+ assert.strictEqual(
+ tr.b.getHotkeyControllerForElement(elementShadow), hkc);
+
+ const subHKC = document.createElement('tv-ui-b-hotkey-controller');
+ Polymer.dom(subElement).appendChild(subHKC);
+
+ assert.strictEqual(
+ tr.b.getHotkeyControllerForElement(subElement), subHKC);
+
+ let didGetCalled = false;
+ subHKC.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keydown',
+ keyCode: 73, useCapture: true,
+ callback() {
+ didGetCalled = true;
+ }
+ }));
+
+ // Ensure it is called when events target the root element.
+ const e = newKeyEvent('keydown', {keyCode: 73});
+ rootElement.dispatchEvent(e);
+ assert.isTrue(didGetCalled);
+ } finally {
+ Polymer.dom(document.body).removeChild(rootElement);
+ }
+ });
+
+ test('inputInsideHKC', function() {
+ const rootElement = document.createElement('div');
+ Polymer.dom(document.body).appendChild(rootElement);
+ try {
+ const elementShadow = rootElement.createShadowRoot();
+
+ const hkc = document.createElement('tv-ui-b-hotkey-controller');
+ Polymer.dom(elementShadow).appendChild(hkc);
+
+ const inputEl = document.createElement('input');
+ Polymer.dom(elementShadow).appendChild(inputEl);
+
+ let didGetCalled = false;
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: 'a'.charCodeAt(0), useCapture: false,
+ callback() {
+ didGetCalled = true;
+ }
+ }));
+
+ // Ensure it is called when events target the root element.
+ didGetCalled = false;
+ let e = newKeyEvent('keypress', {keyCode: 'a'.charCodeAt(0)});
+ rootElement.dispatchEvent(e);
+ assert.isTrue(didGetCalled);
+
+ // Handler should NOT be called when events target the input element.
+ didGetCalled = false;
+ e = newKeyEvent('keypress', {keyCode: 'a'.charCodeAt(0)});
+ inputEl.dispatchEvent(e);
+ assert.isFalse(didGetCalled);
+ } finally {
+ Polymer.dom(document.body).removeChild(rootElement);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html
new file mode 100644
index 00000000000..3a4894278be
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-b-info-bar'>
+ <template>
+ <style>
+ :host {
+ align-items: center;
+ flex: 0 0 auto;
+ background-color: rgb(252, 235, 162);
+ border-bottom: 1px solid #A3A3A3;
+ border-left: 1px solid white;
+ border-right: 1px solid #A3A3A3;
+ border-top: 1px solid white;
+ display: flex;
+ height: 26px;
+ padding: 0 3px 0 3px;
+ }
+
+ :host([hidden]) {
+ display: none !important;
+ }
+
+ #message { flex: 1 1 auto; }
+ </style>
+
+ <span id='message'></span>
+ <span id='buttons'></span>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-info-bar',
+
+ ready() {
+ this.messageEl_ = this.$.message;
+ this.buttonsEl_ = this.$.buttons;
+
+ this.message = '';
+ },
+
+ get message() {
+ return Polymer.dom(this.messageEl_).textContent;
+ },
+
+ set message(message) {
+ Polymer.dom(this.messageEl_).textContent = message;
+ },
+
+ get visible() {
+ return !this.hidden;
+ },
+
+ set visible(visible) {
+ this.hidden = !visible;
+ },
+
+ removeAllButtons() {
+ Polymer.dom(this.buttonsEl_).textContent = '';
+ },
+
+ addButton(text, clickCallback) {
+ const button = document.createElement('button');
+ Polymer.dom(button).textContent = text;
+ button.addEventListener('click', event => clickCallback(event, this));
+ Polymer.dom(this.buttonsEl_).appendChild(button);
+ return button;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html
new file mode 100644
index 00000000000..d095e4d99f9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel='import' href='/tracing/ui/base/info_bar.html'>
+
+<dom-module id='tr-ui-b-info-bar-group'>
+ <template>
+ <style>
+ :host {
+ flex: 0 0 auto;
+ flex-direction: column;
+ display: flex;
+ }
+ </style>
+ <div id='messages'></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-b-info-bar-group',
+
+ ready() {
+ this.messages_ = [];
+ },
+
+ clearMessages() {
+ this.messages_ = [];
+ this.updateContents_();
+ },
+
+ addMessage(text, opt_buttons) {
+ opt_buttons = opt_buttons || [];
+ for (let i = 0; i < opt_buttons.length; i++) {
+ if (opt_buttons[i].buttonText === undefined) {
+ throw new Error('buttonText must be provided');
+ }
+ if (opt_buttons[i].onClick === undefined) {
+ throw new Error('onClick must be provided');
+ }
+ }
+
+ this.messages_.push({
+ text,
+ buttons: opt_buttons || []
+ });
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.messages).textContent = '';
+ this.messages_.forEach(function(message) {
+ const bar = document.createElement('tr-ui-b-info-bar');
+ bar.message = message.text;
+ bar.visible = true;
+
+ message.buttons.forEach(function(button) {
+ bar.addButton(button.buttonText, button.onClick);
+ }, this);
+
+ Polymer.dom(this.$.messages).appendChild(bar);
+ }, this);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html
new file mode 100644
index 00000000000..304d48ede37
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_group_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/info_bar_group.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('group-instantiate', function() {
+ const infoBarGroup = document.createElement('tr-ui-b-info-bar-group');
+ infoBarGroup.addMessage(
+ 'Message 1',
+ [{buttonText: 'ok', onClick() {}}]);
+ infoBarGroup.addMessage(
+ 'Message 2',
+ [{buttonText: 'button 2', onClick() {}}]);
+ this.addHTMLOutput(infoBarGroup);
+ });
+
+ test('group-populate-then-clear', function() {
+ const infoBarGroup = document.createElement('tr-ui-b-info-bar-group');
+ infoBarGroup.addMessage(
+ 'Message 1',
+ [{buttonText: 'ok', onClick() {}}]);
+ infoBarGroup.addMessage(
+ 'Message 2',
+ [{buttonText: 'button 2', onClick() {}}]);
+ infoBarGroup.clearMessages();
+ assert.strictEqual(infoBarGroup.children.length, 0);
+ });
+
+ test('group-populate-clear-repopulate', function() {
+ const infoBarGroup = document.createElement('tr-ui-b-info-bar-group');
+ infoBarGroup.addMessage(
+ 'Message 1',
+ [{buttonText: 'ok', onClick() {}}]);
+ infoBarGroup.addMessage(
+ 'Message 2',
+ [{buttonText: 'button 2', onClick() {}}]);
+ infoBarGroup.clearMessages();
+ infoBarGroup.addMessage(
+ 'Message 1',
+ [{buttonText: 'ok', onClick() {}}]);
+ this.addHTMLOutput(infoBarGroup);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html
new file mode 100644
index 00000000000..94b19482ec8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/info_bar_test.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const infoBar = document.createElement('tr-ui-b-info-bar');
+ infoBar.message = 'This is an info';
+ infoBar.visible = true;
+ this.addHTMLOutput(infoBar);
+ });
+
+ test('buttons', function() {
+ const infoBar = document.createElement('tr-ui-b-info-bar');
+ infoBar.visible = true;
+ infoBar.message = 'This is an info bar with buttons';
+ let didClick = false;
+ const button = infoBar.addButton('More info...', function() {
+ didClick = true;
+ });
+ button.click();
+ assert.isTrue(didClick);
+ this.addHTMLOutput(infoBar);
+ });
+
+ test('hiding', function() {
+ const infoBar = document.createElement('tr-ui-b-info-bar');
+ infoBar.message = 'This is an info bar';
+ infoBar.visible = true;
+ this.addHTMLOutput(infoBar);
+
+ assert.strictEqual(getComputedStyle(infoBar).display, 'flex');
+
+ infoBar.visible = false;
+ assert.strictEqual(getComputedStyle(infoBar).display, 'none');
+
+ infoBar.visible = true;
+ assert.strictEqual(getComputedStyle(infoBar).display, 'flex');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html
new file mode 100644
index 00000000000..e02f4413bb1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/chart_base_2d_brushable_x.html">
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const LineChart = tr.ui.b.define('line-chart', tr.ui.b.ChartBase2DBrushX);
+
+ LineChart.prototype = {
+ __proto__: tr.ui.b.ChartBase2DBrushX.prototype,
+
+ decorate() {
+ super.decorate();
+ this.enableHoverBox = true;
+ this.displayXInHover = false;
+ },
+
+ get defaultGraphWidth() {
+ return 20 * this.data_.length;
+ },
+
+ get defaultGraphHeight() {
+ return 100;
+ },
+
+ drawHoverValueBox_(circle) {
+ tr.ui.b.ColumnChart.prototype.drawHoverValueBox_.call(this, circle);
+ },
+
+ clearHoverValueBox_(circle) {
+ tr.ui.b.ColumnChart.prototype.clearHoverValueBox_.call(this, circle);
+ },
+
+ updateDataContents_(dataSel) {
+ dataSel.selectAll('*').remove();
+ const dataBySeriesKey = this.getDataBySeriesKey_();
+ const seriesKeys = [...this.seriesByKey_.keys()];
+ const pathsSel = dataSel.selectAll('path').data(seriesKeys);
+ pathsSel.enter()
+ .append('path')
+ .style('fill', 'none')
+ .style('stroke-width', '1.5px')
+ .style('stroke', key => this.getDataSeries(key).color)
+ .attr('d', key => {
+ const line = d3.svg.line()
+ .x(d => this.xScale_(d.x))
+ .y(d => this.yScale_(this.dataRange.clamp(d[key])));
+ return line(dataBySeriesKey[key]);
+ });
+ pathsSel.exit().remove();
+
+ if (this.enableHoverBox) {
+ for (let index = 0; index < this.data_.length; ++index) {
+ const datum = this.data_[index];
+ const x = this.getXForDatum_(datum, index);
+ for (const [key, value] of Object.entries(datum)) {
+ if (key === 'x') continue;
+ if (value === undefined) continue;
+ const color = this.getDataSeries(key).color;
+ const circle = document.createElementNS(
+ 'http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', this.xScale_(x));
+ circle.setAttribute('cy',
+ this.yScale_(this.dataRange.clamp(value)));
+ circle.setAttribute('r', 5);
+ circle.style.fill = color;
+ circle.datum = datum;
+ circle.key = key;
+ circle.value = datum[key];
+ circle.leftPx = this.xScale_(x);
+ circle.widthPx = 0;
+ circle.color = color;
+ circle.topPx = this.yScale_(this.dataRange.clamp(value));
+ circle.heightPx = 0;
+ circle.addEventListener(
+ 'mouseenter', () => this.drawHoverValueBox_(circle));
+ circle.addEventListener(
+ 'mouseleave', () => this.clearHoverValueBox_(circle));
+ dataSel[0][0].appendChild(circle);
+ }
+ }
+ }
+ }
+ };
+
+ return {
+ LineChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html
new file mode 100644
index 00000000000..411e46fb5dc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/line_chart_test.html
@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+
+<script>
+'use strict';
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, y: 100},
+ {x: 20, y: 110},
+ {x: 30, y: 100},
+ {x: 40, y: 50}
+ ];
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 100, beta: 50},
+ {x: 20, alpha: 110, beta: 75},
+ {x: 30, alpha: 100, beta: 125},
+ {x: 40, alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: undefined},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 30, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, alpha: 20, beta: 40},
+ {x: 20, alpha: undefined, beta: 10},
+ {x: 30, alpha: 10, beta: undefined},
+ {x: 45, alpha: undefined, beta: 20},
+ {x: 50, alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('brushRangeFromIndices', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 50},
+ {x: 30, value: 60},
+ {x: 70, value: 70},
+ {x: 80, value: 80},
+ {x: 120, value: 90}
+ ];
+ let r = new tr.b.math.Range();
+
+ // Range min should be 10.
+ r = chart.computeBrushRangeFromIndices(-2, 1);
+ assert.strictEqual(r.min, 10);
+
+ // Range max should be 120.
+ r = chart.computeBrushRangeFromIndices(3, 10);
+ assert.strictEqual(r.max, 120);
+
+ // Range should be [10, 120]
+ r = chart.computeBrushRangeFromIndices(-2, 10);
+ assert.strictEqual(r.min, 10);
+ assert.strictEqual(r.max, 120);
+
+ // Range should be [20, 100]
+ r = chart.computeBrushRangeFromIndices(1, 3);
+ assert.strictEqual(r.min, 20);
+ assert.strictEqual(r.max, 100);
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, value: 50},
+ {x: 20, value: 60},
+ {x: 30, value: 80},
+ {x: 40, value: 20},
+ {x: 50, value: 30},
+ {x: 60, value: 20},
+ {x: 70, value: 15},
+ {x: 80, value: 20}
+ ];
+
+ let mouseDownIndex = undefined;
+ let curMouseIndex = undefined;
+
+ function updateBrushedRange() {
+ if (mouseDownIndex === undefined) {
+ chart.brushedRange = new tr.b.math.Range();
+ return;
+ }
+ chart.brushedRange = chart.computeBrushRangeFromIndices(
+ mouseDownIndex, curMouseIndex);
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownIndex = e.index;
+ curMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+ curMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ curMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ });
+
+ test('overrideDataRange', function() {
+ let chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(10, 90);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 100},
+ ];
+
+ chart = new tr.ui.b.LineChart();
+ chart.displayXInHover = true;
+ this.addHTMLOutput(chart);
+ chart.overrideDataRange = tr.b.math.Range.fromExplicitRange(-10, 100);
+ chart.data = [
+ {x: 0, value: 0},
+ {x: 1, value: 50},
+ ];
+ });
+
+ test('sizeInBytes', function() {
+ const chart = new tr.ui.b.LineChart();
+ chart.unit = tr.b.Unit.byName.sizeInBytes;
+ chart.yLogScaleBase = 2;
+ chart.graphHeight = 400;
+ chart.isYLogScale = true;
+ chart.hideLegend = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 0, value: 1},
+ {x: 1, value: 1 << 10},
+ {x: 2, value: 1 << 20},
+ {x: 3, value: 1 << 30},
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html
new file mode 100644
index 00000000000..6e2d1652016
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Simple list view.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * @constructor
+ */
+ const ListView = tr.ui.b.define(
+ 'x-list-view', tr.ui.b.ContainerThatDecoratesItsChildren);
+
+ ListView.prototype = {
+ __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype,
+
+ decorate() {
+ tr.ui.b.ContainerThatDecoratesItsChildren.prototype.decorate.call(this);
+
+ Polymer.dom(this).classList.add('x-list-view');
+ this.style.display = 'block';
+ this.style.userSelect = 'none';
+ this.style.outline = 'none';
+ this.onItemClicked_ = this.onItemClicked_.bind(this);
+ this.onKeyDown_ = this.onKeyDown_.bind(this);
+ this.tabIndex = 0;
+ this.addEventListener('keydown', this.onKeyDown_);
+
+ this.selectionChanged_ = false;
+ },
+
+ decorateChild_(item) {
+ Polymer.dom(item).classList.add('list-item');
+ item.style.paddingTop = '2px';
+ item.style.paddingRight = '4px';
+ item.style.paddingBottom = '2px';
+ item.style.paddingLeft = '4px';
+ item.addEventListener('click', this.onItemClicked_, true);
+
+ Object.defineProperty(
+ item,
+ 'selected', {
+ configurable: true,
+ get: () => item.hasAttribute('selected'),
+ set: value => {
+ // |this| is the ListView.
+ const oldSelection = this.selectedElement;
+ if (oldSelection && oldSelection !== item && value) {
+ Polymer.dom(this.selectedElement).removeAttribute('selected');
+ }
+ if (value) {
+ Polymer.dom(item).setAttribute('selected', 'selected');
+ item.style.backgroundColor = 'rgb(171, 217, 202)';
+ item.style.outline = '1px dotted rgba(0,0,0,0.1)';
+ item.style.outlineOffset = 0;
+ } else {
+ Polymer.dom(item).removeAttribute('selected');
+ item.style.backgroundColor = '';
+ }
+ const newSelection = this.selectedElement;
+ if (newSelection !== oldSelection) {
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ }
+ },
+ });
+ },
+
+ undecorateChild_(item) {
+ this.selectionChanged_ |= item.selected;
+
+ Polymer.dom(item).classList.remove('list-item');
+ item.removeEventListener('click', this.onItemClicked_);
+ delete item.selected;
+ },
+
+ beginDecorating_() {
+ this.selectionChanged_ = false;
+ },
+
+ doneDecoratingForNow_() {
+ if (this.selectionChanged_) {
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ }
+ },
+
+ get selectedElement() {
+ const el = Polymer.dom(this).querySelector('.list-item[selected]');
+ if (!el) return undefined;
+ return el;
+ },
+
+ set selectedElement(el) {
+ if (!el) {
+ if (this.selectedElement) {
+ this.selectedElement.selected = false;
+ }
+ return;
+ }
+
+ if (el.parentElement !== this) {
+ throw new Error(
+ 'Can only select elements that are children of this list view');
+ }
+ el.selected = true;
+ },
+
+ getElementByIndex(index) {
+ return Polymer.dom(this)
+ .querySelector('.list-item:nth-child(' + index + ')');
+ },
+
+ clear() {
+ const changed = this.selectedElement !== undefined;
+ tr.ui.b.ContainerThatDecoratesItsChildren.prototype.clear.call(this);
+ if (changed) {
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ }
+ },
+
+ onItemClicked_(e) {
+ const currentSelectedElement = this.selectedElement;
+ if (currentSelectedElement) {
+ Polymer.dom(currentSelectedElement).removeAttribute('selected');
+ }
+ let element = e.target;
+ while (element.parentElement !== this) {
+ element = element.parentElement;
+ }
+ if (element !== currentSelectedElement) {
+ Polymer.dom(element).setAttribute('selected', 'selected');
+ }
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ },
+
+ onKeyDown_(e) {
+ if (this.selectedElement === undefined) return;
+
+ if (e.keyCode === 38) { // Up arrow.
+ const prev = Polymer.dom(this.selectedElement).previousSibling;
+ if (prev) {
+ prev.selected = true;
+ tr.ui.b.scrollIntoViewIfNeeded(prev);
+ e.preventDefault();
+ return true;
+ }
+ } else if (e.keyCode === 40) { // Down arrow.
+ const next = Polymer.dom(this.selectedElement).nextSibling;
+ if (next) {
+ next.selected = true;
+ tr.ui.b.scrollIntoViewIfNeeded(next);
+ e.preventDefault();
+ return true;
+ }
+ }
+ },
+
+ addItem(textContent) {
+ const item = document.createElement('div');
+ Polymer.dom(item).textContent = textContent;
+ Polymer.dom(this).appendChild(item);
+ item.style.userSelect = 'none';
+ return item;
+ }
+
+ };
+
+ return {
+ ListView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html
new file mode 100644
index 00000000000..685eefc9472
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/list_view_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ListView = tr.ui.b.ListView;
+
+ test('instantiate', function() {
+ const view = new ListView();
+ const i1 = view.addItem('item 1');
+ const i2 = view.addItem('item 2');
+ const i3 = view.addItem('item 3');
+ this.addHTMLOutput(view);
+ });
+
+ test('programmaticSelection', function() {
+ const view = new ListView();
+ const i1 = view.addItem('item 1');
+ const i2 = view.addItem('item 2');
+ const i3 = view.addItem('item 3');
+
+ i2.selected = true;
+ assert.isTrue(i2.hasAttribute('selected'));
+ i3.selected = true;
+ assert.isFalse(i2.hasAttribute('selected'));
+ assert.isTrue(i3.hasAttribute('selected'));
+ });
+
+ test('clickSelection', function() {
+ const view = new ListView();
+ let didFireSelectionChange = false;
+ view.addEventListener('selection-changed', function() {
+ didFireSelectionChange = true;
+ });
+ const i1 = view.addItem('item 1');
+ const i2 = view.addItem('item 2');
+ const i3 = view.addItem('item 3');
+
+ didFireSelectionChange = false;
+ i2.click();
+ assert.isTrue(didFireSelectionChange);
+ assert.strictEqual(view.selectedElement, i2);
+
+ didFireSelectionChange = false;
+ i3.click();
+ assert.isTrue(didFireSelectionChange);
+ assert.strictEqual(view.selectedElement, i3);
+
+ // Click the same target again.
+ didFireSelectionChange = false;
+ i3.click();
+ assert.isTrue(didFireSelectionChange);
+ assert.isUndefined(view.selectedElement);
+
+ didFireSelectionChange = false;
+ i1.click();
+ assert.isTrue(didFireSelectionChange);
+ assert.strictEqual(view.selectedElement, i1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html
new file mode 100644
index 00000000000..bf32a7f25fe
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/mouse_modes.html">
+
+<dom-module id='tr-ui-b-mouse-mode-icon'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ background-image: url(../images/ui-states.png);
+ width: 27px;
+ height: 30px;
+ }
+ :host.active {
+ cursor: auto;
+ }
+ </style>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-mouse-mode-icon',
+
+ properties: {
+ modeName: {
+ type: String,
+ reflectToAttribute: true,
+ observer: 'modeNameChanged'
+ },
+ },
+
+ created() {
+ this.active_ = false;
+ this.acceleratorKey_ = undefined;
+ },
+
+ ready() {
+ this.updateContents_();
+ },
+
+ get mode() {
+ return tr.ui.b.MOUSE_SELECTOR_MODE[this.modeName];
+ },
+
+ set mode(mode) {
+ const modeInfo = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS[mode];
+ if (modeInfo === undefined) {
+ throw new Error('Unknown mode');
+ }
+ this.modeName = modeInfo.name;
+ },
+
+ modeNameChanged() {
+ this.updateContents_();
+ },
+
+ get active() {
+ return this.active_;
+ },
+
+ set active(active) {
+ this.active_ = !!active;
+ if (this.active_) {
+ Polymer.dom(this).classList.add('active');
+ } else {
+ Polymer.dom(this).classList.remove('active');
+ }
+ this.updateContents_();
+ },
+
+ get acceleratorKey() {
+ return this.acceleratorKey_;
+ },
+
+ set acceleratorKey(acceleratorKey) {
+ this.acceleratorKey_ = acceleratorKey;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ if (this.modeName === undefined) return;
+
+ const mode = this.mode;
+ if (mode === undefined) {
+ throw new Error('Invalid mode');
+ }
+
+ const modeInfo = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS[mode];
+ if (!modeInfo) {
+ throw new Error('Invalid mode');
+ }
+
+ let title = modeInfo.title;
+ if (this.acceleratorKey_) {
+ title = title + ' (' + this.acceleratorKey_ + ')';
+ }
+ this.title = title;
+
+ let bp;
+ if (this.active_) {
+ bp = modeInfo.activeBackgroundPosition;
+ } else {
+ bp = modeInfo.defaultBackgroundPosition;
+ }
+ this.style.backgroundPosition = bp;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html
new file mode 100644
index 00000000000..047d5af22b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_icon_test.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE;
+
+ test('inactive', function() {
+ const icon = document.createElement('tr-ui-b-mouse-mode-icon');
+ icon.mode = MOUSE_SELECTOR_MODE.SELECTION;
+ assert.strictEqual(icon.modeName, 'SELECTION');
+ icon.acceleratorKey = 'a';
+ this.addHTMLOutput(icon);
+ });
+
+ test('active', function() {
+ const icon = document.createElement('tr-ui-b-mouse-mode-icon');
+ icon.mode = MOUSE_SELECTOR_MODE.SELECTION;
+ assert.strictEqual(icon.modeName, 'SELECTION');
+ icon.active = true;
+ this.addHTMLOutput(icon);
+ });
+
+ test('modeNameSetter', function() {
+ const icon = document.createElement('tr-ui-b-mouse-mode-icon');
+ Polymer.dom(icon).setAttribute('mode-name', 'SELECTION');
+ this.addHTMLOutput(icon);
+
+ return Promise.resolve().then(function() {
+ assert.strictEqual(icon.mode, 1);
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html
new file mode 100644
index 00000000000..4ee7348833f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector.html
@@ -0,0 +1,577 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html">
+<link rel="import" href="/tracing/ui/base/mouse_modes.html">
+<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<dom-module id='tr-ui-b-mouse-mode-selector'>
+ <template>
+ <style>
+ :host {
+
+ -webkit-user-drag: element;
+ -webkit-user-select: none;
+
+ background: #DDD;
+ border: 1px solid #BBB;
+ border-radius: 4px;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.2);
+ left: calc(100% - 120px);
+ position: absolute;
+ top: 100px;
+ user-select: none;
+ width: 29px;
+ z-index: 20;
+ }
+
+ .drag-handle {
+ background: url(../images/ui-states.png) 2px 3px no-repeat;
+ background-repeat: no-repeat;
+ border-bottom: 1px solid #BCBCBC;
+ cursor: move;
+ display: block;
+ height: 13px;
+ width: 27px;
+ }
+
+ .tool-button {
+ background-position: center center;
+ background-repeat: no-repeat;
+ border-bottom: 1px solid #BCBCBC;
+ border-top: 1px solid #F1F1F1;
+ cursor: pointer;
+ }
+
+ .buttons > .tool-button:last-child {
+ border-bottom: none;
+ }
+
+ </style>
+ <div class="drag-handle"></div>
+ <div class="buttons">
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE;
+ const MOUSE_SELECTOR_MODE_INFOS = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS;
+
+
+ const MIN_MOUSE_SELECTION_DISTANCE = 4;
+
+ const MODIFIER = {
+ SHIFT: 0x1,
+ SPACE: 0x2,
+ CMD_OR_CTRL: 0x4
+ };
+
+ function isCmdOrCtrlPressed(event) {
+ if (tr.isMac) return event.metaKey;
+ return event.ctrlKey;
+ }
+
+ /**
+ * Provides a panel for switching the interaction mode of the mouse.
+ * It handles the user interaction and dispatches events for the various
+ * modes.
+ */
+ Polymer({
+ is: 'tr-ui-b-mouse-mode-selector',
+
+ created() {
+ this.supportedModeMask_ = MOUSE_SELECTOR_MODE.ALL_MODES;
+
+ this.initialRelativeMouseDownPos_ = {x: 0, y: 0};
+
+ this.defaultMode_ = MOUSE_SELECTOR_MODE.PANSCAN;
+ this.settingsKey_ = undefined;
+ this.mousePos_ = {x: 0, y: 0};
+ this.mouseDownPos_ = {x: 0, y: 0};
+
+ this.onMouseDown_ = this.onMouseDown_.bind(this);
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.onMouseUp_ = this.onMouseUp_.bind(this);
+
+ this.onKeyDown_ = this.onKeyDown_.bind(this);
+ this.onKeyUp_ = this.onKeyUp_.bind(this);
+
+ this.mode_ = undefined;
+ this.modeToKeyCodeMap_ = {};
+ this.modifierToModeMap_ = {};
+
+ this.targetElement_ = undefined;
+ this.modeBeforeAlternativeModeActivated_ = null;
+
+ this.isInteracting_ = false;
+ this.isClick_ = false;
+ },
+
+ ready() {
+ this.buttonsEl_ = Polymer.dom(this.root).querySelector('.buttons');
+ this.dragHandleEl_ = Polymer.dom(this.root).querySelector(
+ '.drag-handle');
+ this.supportedModeMask = MOUSE_SELECTOR_MODE.ALL_MODES;
+
+ this.dragHandleEl_.addEventListener('mousedown',
+ this.onDragHandleMouseDown_.bind(this));
+
+ this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_);
+ this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_);
+ this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this));
+ },
+
+ attached() {
+ document.addEventListener('keydown', this.onKeyDown_);
+ document.addEventListener('keyup', this.onKeyUp_);
+ },
+
+ detached() {
+ document.removeEventListener('keydown', this.onKeyDown_);
+ document.removeEventListener('keyup', this.onKeyUp_);
+ },
+
+ get targetElement() {
+ return this.targetElement_;
+ },
+
+ set targetElement(target) {
+ if (this.targetElement_) {
+ this.targetElement_.removeEventListener('mousedown', this.onMouseDown_);
+ }
+ this.targetElement_ = target;
+ if (this.targetElement_) {
+ this.targetElement_.addEventListener('mousedown', this.onMouseDown_);
+ }
+ },
+
+ get defaultMode() {
+ return this.defaultMode_;
+ },
+
+ set defaultMode(defaultMode) {
+ this.defaultMode_ = defaultMode;
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ this.settingsKey_ = settingsKey;
+ if (!this.settingsKey_) return;
+
+ let mode = tr.b.Settings.get(this.settingsKey_ + '.mode', undefined);
+ // Modes changed from 1,2,3,4 to 0x1, 0x2, 0x4, 0x8. Fix any stray
+ // settings to the best of our abilities.
+ if (MOUSE_SELECTOR_MODE_INFOS[mode] === undefined) {
+ mode = undefined;
+ }
+
+ // Restoring settings against unsupported modes should just go back to the
+ // default mode.
+ if ((mode & this.supportedModeMask_) === 0) {
+ mode = undefined;
+ }
+
+ if (!mode) mode = this.defaultMode_;
+ this.mode = mode;
+
+ const pos = tr.b.Settings.get(this.settingsKey_ + '.pos', undefined);
+ if (pos) this.pos = pos;
+ },
+
+ get supportedModeMask() {
+ return this.supportedModeMask_;
+ },
+
+ /**
+ * Sets the supported modes. Should be an OR-ing of MOUSE_SELECTOR_MODE
+ * values.
+ */
+ set supportedModeMask(supportedModeMask) {
+ if (this.mode && (supportedModeMask & this.mode) === 0) {
+ throw new Error('supportedModeMask must include current mode.');
+ }
+
+ function createButtonForMode(mode) {
+ return button;
+ }
+
+ this.supportedModeMask_ = supportedModeMask;
+ Polymer.dom(this.buttonsEl_).textContent = '';
+ for (const modeName in MOUSE_SELECTOR_MODE) {
+ if (modeName === 'ALL_MODES') continue;
+
+ const mode = MOUSE_SELECTOR_MODE[modeName];
+ if ((this.supportedModeMask_ & mode) === 0) continue;
+
+ const button = document.createElement('tr-ui-b-mouse-mode-icon');
+ button.mode = mode;
+ Polymer.dom(button).classList.add('tool-button');
+
+ Polymer.dom(this.buttonsEl_).appendChild(button);
+ }
+ },
+
+ getButtonForMode_(mode) {
+ for (let i = 0; i < this.buttonsEl_.children.length; i++) {
+ const buttonEl = this.buttonsEl_.children[i];
+ if (buttonEl.mode === mode) {
+ return buttonEl;
+ }
+ }
+ return undefined;
+ },
+
+ get mode() {
+ return this.currentMode_;
+ },
+
+ set mode(newMode) {
+ if (newMode !== undefined) {
+ if (typeof newMode !== 'number') {
+ throw new Error('Mode must be a number');
+ }
+ if ((newMode & this.supportedModeMask_) === 0) {
+ throw new Error('Cannot switch to this mode, it is not supported');
+ }
+ if (MOUSE_SELECTOR_MODE_INFOS[newMode] === undefined) {
+ throw new Error('Unrecognized mode');
+ }
+ }
+
+ let modeInfo;
+
+ if (this.currentMode_ === newMode) return;
+
+ if (this.currentMode_) {
+ const buttonEl = this.getButtonForMode_(this.currentMode_);
+ if (buttonEl) buttonEl.active = false;
+
+ // End event.
+ if (this.isInteracting_) {
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end);
+ this.dispatchEvent(mouseEvent);
+ }
+
+ // Exit event.
+ modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_];
+ tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true);
+ }
+
+ this.currentMode_ = newMode;
+
+ if (this.currentMode_) {
+ const buttonEl = this.getButtonForMode_(this.currentMode_);
+ if (buttonEl) buttonEl.active = true;
+
+ // Entering a new mode resets mouse down pos.
+ this.mouseDownPos_.x = this.mousePos_.x;
+ this.mouseDownPos_.y = this.mousePos_.y;
+
+ // Enter event.
+ modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_];
+ if (!this.isInAlternativeMode_) {
+ tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true);
+ }
+
+ // Begin event.
+ if (this.isInteracting_) {
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin);
+ this.dispatchEvent(mouseEvent);
+ }
+ }
+
+ if (this.settingsKey_ && !this.isInAlternativeMode_) {
+ tr.b.Settings.set(this.settingsKey_ + '.mode', this.mode);
+ }
+ },
+
+ setKeyCodeForMode(mode, keyCode) {
+ if ((mode & this.supportedModeMask_) === 0) {
+ throw new Error('Mode not supported');
+ }
+ this.modeToKeyCodeMap_[mode] = keyCode;
+
+ if (!this.buttonsEl_) return;
+
+ const buttonEl = this.getButtonForMode_(mode);
+ if (buttonEl) {
+ buttonEl.acceleratorKey = String.fromCharCode(keyCode);
+ }
+ },
+
+ setCurrentMousePosFromEvent_(e) {
+ this.mousePos_.x = e.clientX;
+ this.mousePos_.y = e.clientY;
+ },
+
+ createEvent_(eventName, sourceEvent) {
+ const event = new tr.b.Event(eventName, true);
+ event.clientX = this.mousePos_.x;
+ event.clientY = this.mousePos_.y;
+ event.deltaX = this.mousePos_.x - this.mouseDownPos_.x;
+ event.deltaY = this.mousePos_.y - this.mouseDownPos_.y;
+ event.mouseDownX = this.mouseDownPos_.x;
+ event.mouseDownY = this.mouseDownPos_.y;
+ event.didPreventDefault = false;
+ event.preventDefault = function() {
+ event.didPreventDefault = true;
+ if (sourceEvent) {
+ sourceEvent.preventDefault();
+ }
+ };
+ event.stopPropagation = function() {
+ sourceEvent.stopPropagation();
+ };
+ event.stopImmediatePropagation = function() {
+ throw new Error('Not implemented');
+ };
+ return event;
+ },
+
+ onMouseDown_(e) {
+ if (e.button !== 0) return;
+ this.setCurrentMousePosFromEvent_(e);
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin, e);
+ if (this.mode === MOUSE_SELECTOR_MODE.SELECTION) {
+ mouseEvent.appendSelection = isCmdOrCtrlPressed(e);
+ }
+ this.dispatchEvent(mouseEvent);
+ this.isInteracting_ = true;
+ this.isClick_ = true;
+ tr.ui.b.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_);
+ },
+
+ onMouseMove_(e) {
+ this.setCurrentMousePosFromEvent_(e);
+
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.update, e);
+ this.dispatchEvent(mouseEvent);
+
+ if (this.isInteracting_) {
+ this.checkIsClick_(e);
+ }
+ },
+
+ onMouseUp_(e) {
+ if (e.button !== 0) return;
+
+ const mouseEvent = this.createEvent_(
+ MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end, e);
+ mouseEvent.isClick = this.isClick_;
+ this.dispatchEvent(mouseEvent);
+
+ if (this.isClick_ && !mouseEvent.didPreventDefault) {
+ this.dispatchClickEvents_(e);
+ }
+
+ this.isInteracting_ = false;
+ this.updateAlternativeModeState_(e);
+ },
+
+ onButtonMouseDown_(e) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ },
+
+ onButtonMouseUp_(e) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ },
+
+ onButtonPress_(e) {
+ this.modeBeforeAlternativeModeActivated_ = undefined;
+ this.mode = e.target.mode;
+ e.preventDefault();
+ },
+
+ onKeyDown_(e) {
+ // Keys dispatched to INPUT elements still bubble, even when they're
+ // handled. So, skip any events that targeted the input element.
+ if (e.path[0].tagName === 'INPUT') return;
+
+ if (e.keyCode === ' '.charCodeAt(0)) {
+ this.spacePressed_ = true;
+ }
+ this.updateAlternativeModeState_(e);
+ },
+
+ onKeyUp_(e) {
+ // Keys dispatched to INPUT elements still bubble, even when they're
+ // handled. So, skip any events that targeted the input element.
+ if (e.path[0].tagName === 'INPUT') return;
+
+ if (e.keyCode === ' '.charCodeAt(0)) {
+ this.spacePressed_ = false;
+ }
+
+ let didHandleKey = false;
+ for (const [modeStr, keyCode] of Object.entries(this.modeToKeyCodeMap_)) {
+ if (e.keyCode === keyCode) {
+ this.modeBeforeAlternativeModeActivated_ = undefined;
+ const mode = parseInt(modeStr);
+ this.mode = mode;
+ didHandleKey = true;
+ }
+ }
+
+ if (didHandleKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ }
+ this.updateAlternativeModeState_(e);
+ },
+
+ updateAlternativeModeState_(e) {
+ const shiftPressed = e.shiftKey;
+ const spacePressed = this.spacePressed_;
+ const cmdOrCtrlPressed = isCmdOrCtrlPressed(e);
+
+ // Figure out the new mode
+ const smm = this.supportedModeMask_;
+ let newMode;
+ let isNewModeAnAlternativeMode = false;
+ if (shiftPressed &&
+ (this.modifierToModeMap_[MODIFIER.SHIFT] & smm) !== 0) {
+ newMode = this.modifierToModeMap_[MODIFIER.SHIFT];
+ isNewModeAnAlternativeMode = true;
+ } else if (spacePressed &&
+ (this.modifierToModeMap_[MODIFIER.SPACE] & smm) !== 0) {
+ newMode = this.modifierToModeMap_[MODIFIER.SPACE];
+ isNewModeAnAlternativeMode = true;
+ } else if (cmdOrCtrlPressed &&
+ (this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL] & smm) !== 0) {
+ newMode = this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL];
+ isNewModeAnAlternativeMode = true;
+ } else {
+ // Go to the old mode, if there is one.
+ if (this.isInAlternativeMode_) {
+ newMode = this.modeBeforeAlternativeModeActivated_;
+ isNewModeAnAlternativeMode = false;
+ } else {
+ newMode = undefined;
+ }
+ }
+
+ // Maybe a mode change isn't needed.
+ if (this.mode === newMode || newMode === undefined) return;
+
+ // Okay, we're changing.
+ if (isNewModeAnAlternativeMode) {
+ this.modeBeforeAlternativeModeActivated_ = this.mode;
+ }
+ this.mode = newMode;
+ },
+
+ get isInAlternativeMode_() {
+ return !!this.modeBeforeAlternativeModeActivated_;
+ },
+
+ setModifierForAlternateMode(mode, modifier) {
+ this.modifierToModeMap_[modifier] = mode;
+ },
+
+ get pos() {
+ return {
+ x: parseInt(this.style.left),
+ y: parseInt(this.style.top)
+ };
+ },
+
+ set pos(pos) {
+ pos = this.constrainPositionToBounds_(pos);
+
+ this.style.left = pos.x + 'px';
+ this.style.top = pos.y + 'px';
+
+ if (this.settingsKey_) {
+ tr.b.Settings.set(this.settingsKey_ + '.pos', this.pos);
+ }
+ },
+
+ constrainPositionToBounds_(pos) {
+ const parent = this.offsetParent || document.body;
+ const parentRect = tr.ui.b.windowRectForElement(parent);
+
+ const top = 0;
+ const bottom = parentRect.height - this.offsetHeight;
+ const left = 0;
+ const right = parentRect.width - this.offsetWidth;
+
+ const res = {};
+ res.x = Math.max(pos.x, left);
+ res.x = Math.min(res.x, right);
+
+ res.y = Math.max(pos.y, top);
+ res.y = Math.min(res.y, bottom);
+ return res;
+ },
+
+ onDragHandleMouseDown_(e) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+
+ const mouseDownPos = {
+ x: e.clientX - this.offsetLeft,
+ y: e.clientY - this.offsetTop
+ };
+ tr.ui.b.trackMouseMovesUntilMouseUp(function(e) {
+ const pos = {};
+ pos.x = e.clientX - mouseDownPos.x;
+ pos.y = e.clientY - mouseDownPos.y;
+ this.pos = pos;
+ }.bind(this));
+ },
+
+ checkIsClick_(e) {
+ if (!this.isInteracting_ || !this.isClick_) return;
+
+ const deltaX = this.mousePos_.x - this.mouseDownPos_.x;
+ const deltaY = this.mousePos_.y - this.mouseDownPos_.y;
+ const minDist = MIN_MOUSE_SELECTION_DISTANCE;
+
+ if (deltaX * deltaX + deltaY * deltaY > minDist * minDist) {
+ this.isClick_ = false;
+ }
+ },
+
+ dispatchClickEvents_(e) {
+ if (!this.isClick_) return;
+
+ const modeInfo = MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.SELECTION];
+ const eventNames = modeInfo.eventNames;
+
+ let mouseEvent = this.createEvent_(eventNames.begin);
+ mouseEvent.appendSelection = isCmdOrCtrlPressed(e);
+ this.dispatchEvent(mouseEvent);
+
+ mouseEvent = this.createEvent_(eventNames.end);
+ this.dispatchEvent(mouseEvent);
+ }
+ });
+
+ return {
+ MIN_MOUSE_SELECTION_DISTANCE,
+ MODIFIER,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html
new file mode 100644
index 00000000000..577d40dbae3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_mode_selector_test.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE;
+ test('instantiate', function() {
+ const sel = document.createElement('tr-ui-b-mouse-mode-selector');
+ sel.supportedModeMask =
+ MOUSE_SELECTOR_MODE.SELECTION |
+ MOUSE_SELECTOR_MODE.PANSCAN;
+ this.addHTMLOutput(sel);
+ });
+
+ test('changeMaskWithUnsupportedMode', function() {
+ const sel = document.createElement('tr-ui-b-mouse-mode-selector');
+ sel.mode = MOUSE_SELECTOR_MODE.SELECTION;
+ assert.throw(function() {
+ sel.supportedModeMask = MOUSE_SELECTOR_MODE.ZOOM;
+ });
+ });
+
+ test('modePersists', function() {
+ const sel1 = document.createElement('tr-ui-b-mouse-mode-selector');
+ sel1.defaultMode_ = MOUSE_SELECTOR_MODE.ZOOM;
+ sel1.settingsKey = 'foo';
+ assert.strictEqual(sel1.mode, MOUSE_SELECTOR_MODE.ZOOM);
+
+ sel1.mode = MOUSE_SELECTOR_MODE.PANSCAN;
+
+ const sel2 = document.createElement('tr-ui-b-mouse-mode-selector');
+ sel2.settingsKey = 'foo';
+ assert.strictEqual(sel2.mode, MOUSE_SELECTOR_MODE.PANSCAN);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html
new file mode 100644
index 00000000000..8d68f0279cc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_modes.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const MOUSE_SELECTOR_MODE = {};
+ MOUSE_SELECTOR_MODE.SELECTION = 0x1;
+ MOUSE_SELECTOR_MODE.PANSCAN = 0x2;
+ MOUSE_SELECTOR_MODE.ZOOM = 0x4;
+ MOUSE_SELECTOR_MODE.TIMING = 0x8;
+ MOUSE_SELECTOR_MODE.ROTATE = 0x10;
+ MOUSE_SELECTOR_MODE.ALL_MODES = 0x1F;
+
+ const MOUSE_SELECTOR_MODE_INFOS = {};
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.PANSCAN] = {
+ name: 'PANSCAN',
+ mode: MOUSE_SELECTOR_MODE.PANSCAN,
+ title: 'pan',
+ eventNames: {
+ enter: 'enterpan',
+ begin: 'beginpan',
+ update: 'updatepan',
+ end: 'endpan',
+ exit: 'exitpan'
+ },
+ activeBackgroundPosition: '-30px -10px',
+ defaultBackgroundPosition: '0 -10px'
+ };
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.SELECTION] = {
+ name: 'SELECTION',
+ mode: MOUSE_SELECTOR_MODE.SELECTION,
+ title: 'selection',
+ eventNames: {
+ enter: 'enterselection',
+ begin: 'beginselection',
+ update: 'updateselection',
+ end: 'endselection',
+ exit: 'exitselection'
+ },
+ activeBackgroundPosition: '-30px -40px',
+ defaultBackgroundPosition: '0 -40px'
+ };
+
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.ZOOM] = {
+ name: 'ZOOM',
+ mode: MOUSE_SELECTOR_MODE.ZOOM,
+ title: 'zoom',
+ eventNames: {
+ enter: 'enterzoom',
+ begin: 'beginzoom',
+ update: 'updatezoom',
+ end: 'endzoom',
+ exit: 'exitzoom'
+ },
+ activeBackgroundPosition: '-30px -70px',
+ defaultBackgroundPosition: '0 -70px'
+ };
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.TIMING] = {
+ name: 'TIMING',
+ mode: MOUSE_SELECTOR_MODE.TIMING,
+ title: 'timing',
+ eventNames: {
+ enter: 'entertiming',
+ begin: 'begintiming',
+ update: 'updatetiming',
+ end: 'endtiming',
+ exit: 'exittiming'
+ },
+ activeBackgroundPosition: '-30px -100px',
+ defaultBackgroundPosition: '0 -100px'
+ };
+ MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.ROTATE] = {
+ name: 'ROTATE',
+ mode: MOUSE_SELECTOR_MODE.ROTATE,
+ title: 'rotate',
+ eventNames: {
+ enter: 'enterrotate',
+ begin: 'beginrotate',
+ update: 'updaterotate',
+ end: 'endrotate',
+ exit: 'exitrotate'
+ },
+ activeBackgroundPosition: '-30px -130px',
+ defaultBackgroundPosition: '0 -130px'
+ };
+
+ return {
+ MOUSE_SELECTOR_MODE_INFOS,
+ MOUSE_SELECTOR_MODE,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html
new file mode 100644
index 00000000000..f71e0ba2dea
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/mouse_tracker.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview A Mouse-event abtraction that waits for
+ * mousedown, then watches for subsequent mousemove events
+ * until the next mouseup event, then waits again.
+ * State changes are signaled with
+ * 'mouse-tracker-start' : mousedown and tracking
+ * 'mouse-tracker-move' : mouse move
+ * 'mouse-tracker-end' : mouseup and not tracking.
+ */
+
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * @constructor
+ * @param {HTMLElement} targetElement will recv events 'mouse-tracker-start',
+ * 'mouse-tracker-move', 'mouse-tracker-end'.
+ */
+ function MouseTracker(opt_targetElement) {
+ this.onMouseDown_ = this.onMouseDown_.bind(this);
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.onMouseUp_ = this.onMouseUp_.bind(this);
+
+ this.targetElement = opt_targetElement;
+ }
+
+ MouseTracker.prototype = {
+
+ get targetElement() {
+ return this.targetElement_;
+ },
+
+ set targetElement(targetElement) {
+ if (this.targetElement_) {
+ this.targetElement_.removeEventListener('mousedown', this.onMouseDown_);
+ }
+ this.targetElement_ = targetElement;
+ if (this.targetElement_) {
+ this.targetElement_.addEventListener('mousedown', this.onMouseDown_);
+ }
+ },
+
+ onMouseDown_(e) {
+ if (e.button !== 0) return true;
+
+ e = this.remakeEvent_(e, 'mouse-tracker-start');
+ this.targetElement_.dispatchEvent(e);
+ document.addEventListener('mousemove', this.onMouseMove_);
+ document.addEventListener('mouseup', this.onMouseUp_);
+ this.targetElement_.addEventListener('blur', this.onMouseUp_);
+ this.savePreviousUserSelect_ = document.body.style['-webkit-user-select'];
+ document.body.style['-webkit-user-select'] = 'none';
+ e.preventDefault();
+ return true;
+ },
+
+ onMouseMove_(e) {
+ e = this.remakeEvent_(e, 'mouse-tracker-move');
+ this.targetElement_.dispatchEvent(e);
+ },
+
+ onMouseUp_(e) {
+ document.removeEventListener('mousemove', this.onMouseMove_);
+ document.removeEventListener('mouseup', this.onMouseUp_);
+ this.targetElement_.removeEventListener('blur', this.onMouseUp_);
+ document.body.style['-webkit-user-select'] =
+ this.savePreviousUserSelect_;
+ e = this.remakeEvent_(e, 'mouse-tracker-end');
+ this.targetElement_.dispatchEvent(e);
+ },
+
+ remakeEvent_(e, newType) {
+ const remade = new tr.b.Event(newType, true, true);
+ remade.x = e.x;
+ remade.y = e.y;
+ remade.offsetX = e.offsetX;
+ remade.offsetY = e.offsetY;
+ remade.clientX = e.clientX;
+ remade.clientY = e.clientY;
+ return remade;
+ }
+
+ };
+
+ function trackMouseMovesUntilMouseUp(mouseMoveHandler,
+ opt_mouseUpHandler, opt_keyUpHandler) {
+ function cleanupAndDispatchToMouseUp(e) {
+ document.removeEventListener('mousemove', mouseMoveHandler);
+ if (opt_keyUpHandler) {
+ document.removeEventListener('keyup', opt_keyUpHandler);
+ }
+ document.removeEventListener('mouseup', cleanupAndDispatchToMouseUp);
+ if (opt_mouseUpHandler) {
+ opt_mouseUpHandler(e);
+ }
+ }
+ document.addEventListener('mousemove', mouseMoveHandler);
+ if (opt_keyUpHandler) {
+ document.addEventListener('keyup', opt_keyUpHandler);
+ }
+ document.addEventListener('mouseup', cleanupAndDispatchToMouseUp);
+ }
+
+ return {
+ MouseTracker,
+ trackMouseMovesUntilMouseUp,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html
new file mode 100644
index 00000000000..19bda4bcec8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/bar_chart.html">
+<link rel="import" href="/tracing/ui/base/d3.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const NameBarChart = tr.ui.b.define('name-bar-chart', tr.ui.b.BarChart);
+
+ const Y_AXIS_PADDING = 2;
+
+ NameBarChart.prototype = {
+ __proto__: tr.ui.b.BarChart.prototype,
+
+ getDataPointAtChartPoint_(chartPoint) {
+ return {
+ x: tr.ui.b.BarChart.prototype.getDataPointAtChartPoint_.call(
+ this, chartPoint).x,
+ y: parseInt(Math.floor(
+ (this.graphHeight - chartPoint.y) / this.barHeight))
+ };
+ },
+
+ getXForDatum_(datum, index) {
+ return index;
+ },
+
+ get yAxisWidth() {
+ if (this.data.length === 0) return 0;
+ return Y_AXIS_PADDING + tr.b.math.Statistics.max(
+ this.data_, d => tr.ui.b.getSVGTextSize(this, d.x).width);
+ },
+
+ get defaultGraphHeight() {
+ return (3 + this.textHeightPx_) * this.data.length;
+ },
+
+ updateYAxis_(yAxis) {
+ // Building the y-axis requires measuring text.
+ // If necessary, wait for this element to be displayed.
+ if (tr.ui.b.getSVGTextSize(this, 'test').width === 0) {
+ tr.b.requestAnimationFrame(() => this.updateYAxis_(yAxis));
+ return;
+ }
+
+ // When we can measure text, we're ready to build the y-axis.
+ yAxis.selectAll('*').remove();
+ if (this.hideYAxis) return;
+ const nameTexts = yAxis.selectAll('text').data(this.data_);
+ nameTexts
+ .enter()
+ .append('text')
+ .attr('x', d => -(
+ tr.ui.b.getSVGTextSize(this, d.x).width + Y_AXIS_PADDING))
+ .attr('y', (d, index) => this.verticalScale_(index))
+ .text(d => d.x);
+ nameTexts.exit().remove();
+
+ let previousTop = undefined;
+ for (const text of nameTexts[0]) {
+ const bbox = text.getBBox();
+ if ((previousTop === undefined) ||
+ (previousTop > (bbox.y + bbox.height))) {
+ previousTop = bbox.y;
+ } else {
+ text.style.opacity = 0;
+ }
+ }
+ }
+ };
+
+ return {
+ NameBarChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html
new file mode 100644
index 00000000000..771022a86d9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_bar_chart_test.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/name_bar_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 100},
+ {x: 'ball', value: 110},
+ {x: 'cat', value: 100},
+ {x: 'dog', value: 50}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 100, beta: 50},
+ {x: 'ball', alpha: 110, beta: 75},
+ {x: 'cat', alpha: 100, beta: 125},
+ {x: 'dog', alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: undefined},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: 40},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 50},
+ {x: 'ball', value: 60},
+ {x: 'cat', value: 80},
+ {x: 'dog', value: 20},
+ {x: 'echo', value: 30},
+ {x: 'fortune', value: 20},
+ {x: 'gpu', value: 15},
+ {x: 'happy', value: 20}
+ ];
+
+ let mouseDownIndex = undefined;
+ let currentMouseIndex = undefined;
+
+ function updateBrushedRange() {
+ const r = new tr.b.math.Range();
+ r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex));
+ r.max = Math.min(chart.data.length, Math.max(mouseDownIndex,
+ currentMouseIndex) + 1);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownIndex = e.index;
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ });
+
+ test('instantiation_hideXandYAxis', function() {
+ const chart = new tr.ui.b.NameBarChart();
+ chart.hideXAxis = true;
+ chart.hideYAxis = true;
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 100},
+ {x: 'ball', value: 110},
+ {x: 'cat', value: 100},
+ {x: 'dog', value: 50}
+ ];
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html
new file mode 100644
index 00000000000..6c0144da7ce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/column_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const MIN_GUIDELINE_HEIGHT_PX = 3;
+
+ const CHECKBOX_WIDTH_PX = 18;
+
+ const NameColumnChart = tr.ui.b.define(
+ 'name-column-chart', tr.ui.b.ColumnChart);
+
+ NameColumnChart.prototype = {
+ __proto__: tr.ui.b.ColumnChart.prototype,
+
+ get xAxisHeight() {
+ // Add 5px for descenders because SVG draws text baselines at the
+ // specified y-coordinate.
+ return 5 + (this.textHeightPx_ * this.data_.length);
+ },
+
+ updateMargins_() {
+ super.updateMargins_();
+ let xAxisTickOverhangPx = 0;
+ for (let i = 0; i < this.data_.length; ++i) {
+ const datum = this.data_[i];
+ xAxisTickOverhangPx = Math.max(xAxisTickOverhangPx,
+ this.xScale_(i) + tr.ui.b.getSVGTextSize(this, datum.x).width -
+ this.graphWidth);
+ }
+ this.margin.right = Math.max(this.margin.right, xAxisTickOverhangPx);
+ },
+
+ getXForDatum_(datum, index) {
+ return index;
+ },
+
+ get xAxisTickOffset() {
+ return 0.5;
+ },
+
+ updateXAxis_(xAxis) {
+ xAxis.selectAll('*').remove();
+ if (this.hideXAxis) return;
+
+ // Draw the tick labels from |this.data_[*].x|.
+ // Lay them out so that the text doesn't overlap.
+ // They may overhang into |this.margin.right|.
+ const nameTexts = xAxis.selectAll('text')
+ .data(this.data_);
+ nameTexts
+ .enter()
+ .append('text')
+ .attr('transform', (d, index) => 'translate(0, ' +
+ this.textHeightPx_ * (this.data_.length - index) + ')')
+ .attr('x', (d, index) => this.xScale_(index))
+ .attr('y', d => this.graphHeight)
+ .text(d => d.x);
+ nameTexts.exit().remove();
+
+ // Draw lines to guide the eye from bottom center of the column to the
+ // tick label.
+ const guideLines = xAxis.selectAll('line.guide').data(this.data_);
+ guideLines.enter()
+ .append('line')
+ .attr('x1', (d, index) => this.xScale_(index + this.xAxisTickOffset))
+ .attr('x2', (d, index) => this.xScale_(index + this.xAxisTickOffset))
+ .attr('y1', () => this.graphHeight)
+ .attr('y2', (d, index) => this.graphHeight + Math.max(
+ MIN_GUIDELINE_HEIGHT_PX,
+ (this.textHeightPx_ * (this.data_.length - index - 1))));
+ }
+ };
+
+ return {
+ NameColumnChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html
new file mode 100644
index 00000000000..5e78dd6aa76
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_column_chart_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/name_column_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ this.addHTMLOutput(document.createTextNode(
+ 'There should be a capital "A" at the end of the string of "a"s.'));
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ // Make a x-axis tick label long enough that it would overhang past the
+ // right edge of the legend in order to test that updateMargins_ extends the
+ // right margin.
+ chart.data = [
+ {x: 'a'.repeat(20) + 'A', value: 100},
+ {x: 'b', value: 110},
+ {x: 'c', value: 100},
+ {x: 'd', value: 50}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 100, beta: 50},
+ {x: 'ball', alpha: 110, beta: 75},
+ {x: 'cat', alpha: 100, beta: 125},
+ {x: 'dog', alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: undefined},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: 40},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.NameColumnChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 50},
+ {x: 'ball', value: 60},
+ {x: 'cat', value: 80},
+ {x: 'dog', value: 20},
+ {x: 'echo', value: 30},
+ {x: 'fortune', value: 20},
+ {x: 'gpu', value: 15},
+ {x: 'happy', value: 20}
+ ];
+
+ let mouseDownIndex = undefined;
+ let currentMouseIndex = undefined;
+
+ function updateBrushedRange() {
+ const r = new tr.b.math.Range();
+ r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex));
+ r.max = Math.min(chart.data.length,
+ Math.max(mouseDownIndex, currentMouseIndex) + 1);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownIndex = e.index;
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html
new file mode 100644
index 00000000000..572ba36e533
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+<link rel="import" href="/tracing/ui/base/name_column_chart.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const LineChart = tr.ui.b.LineChart;
+
+ // @constructor
+ const NameLineChart = tr.ui.b.define('name-line-chart', LineChart);
+
+ NameLineChart.prototype = {
+ __proto__: LineChart.prototype,
+
+ getXForDatum_(datum, index) {
+ return index;
+ },
+
+ get xAxisHeight() {
+ // Add 5px for descenders because SVG draws text baselines at the
+ // specified y-coordinate.
+ return 5 + (this.textHeightPx_ * this.data_.length);
+ },
+
+ get xAxisTickOffset() {
+ return 0;
+ },
+
+ updateMargins_() {
+ tr.ui.b.NameColumnChart.prototype.updateMargins_.call(this);
+ },
+
+ updateXAxis_(xAxis) {
+ xAxis.selectAll('*').remove();
+ if (this.hideXAxis) return;
+
+ tr.ui.b.NameColumnChart.prototype.updateXAxis_.call(this, xAxis);
+
+ const baseline = xAxis.selectAll('path').data([this]);
+ baseline.enter().append('line')
+ .attr('stroke', 'black')
+ .attr('x1', this.xScale_(0))
+ .attr('x2', this.xScale_(this.data_.length - 1))
+ .attr('y1', this.graphHeight)
+ .attr('y2', this.graphHeight);
+ baseline.exit().remove();
+ }
+ };
+
+ return {
+ NameLineChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html
new file mode 100644
index 00000000000..fa7388103ae
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/name_line_chart_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/name_line_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 100},
+ {x: 'ball', value: 110},
+ {x: 'cat', value: 100},
+ {x: 'dog', value: 50}
+ ];
+ });
+
+ test('undefined', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ assert.throws(function() {
+ chart.data = undefined;
+ });
+ });
+
+ test('instantiation_twoSeries', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 100, beta: 50},
+ {x: 'ball', alpha: 110, beta: 75},
+ {x: 'cat', alpha: 100, beta: 125},
+ {x: 'dog', alpha: 50, beta: 125}
+ ];
+
+ const r = new tr.b.math.Range();
+ r.addValue(20);
+ r.addValue(40);
+ chart.brushedRange = r;
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueSparse', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: undefined},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: 30}
+ ];
+ });
+
+ test('instantiation_twoSparseSeriesWithFirstValueNotSparse', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', alpha: 20, beta: 40},
+ {x: 'ball', alpha: undefined, beta: 10},
+ {x: 'cat', alpha: 10, beta: undefined},
+ {x: 'dog', alpha: undefined, beta: 20},
+ {x: 'echo', alpha: 30, beta: undefined}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.NameLineChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 'apple', value: 50},
+ {x: 'ball', value: 60},
+ {x: 'cat', value: 80},
+ {x: 'dog', value: 20},
+ {x: 'echo', value: 30},
+ {x: 'fortune', value: 20},
+ {x: 'gpu', value: 15},
+ {x: 'happy', value: 20}
+ ];
+
+ let mouseDownIndex = undefined;
+ let currentMouseIndex = undefined;
+
+ function updateBrushedRange() {
+ const r = new tr.b.math.Range();
+ r.min = Math.max(0, Math.min(mouseDownIndex, currentMouseIndex));
+ r.max = Math.min(chart.data.length, Math.max(mouseDownIndex,
+ currentMouseIndex) + 1);
+ chart.brushedRange = r;
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDownIndex = e.index;
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ currentMouseIndex = e.index;
+ updateBrushedRange();
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html
new file mode 100644
index 00000000000..9b1014d63b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay.html
@@ -0,0 +1,351 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<template id="overlay-template">
+ <style>
+ overlay-mask {
+ left: 0;
+ padding: 8px;
+ position: absolute;
+ top: 0;
+ z-index: 1000;
+ font-family: sans-serif;
+ -webkit-justify-content: center;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ height: 100%;
+ left: 0;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ }
+ overlay-mask:focus {
+ outline: none;
+ }
+ overlay-vertical-centering-container {
+ -webkit-justify-content: center;
+ flex-direction: column;
+ display: flex;
+ }
+ overlay-frame {
+ z-index: 1100;
+ background: rgb(255, 255, 255);
+ border: 1px solid #ccc;
+ margin: 75px;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ }
+ title-bar {
+ -webkit-align-items: center;
+ flex-direction: row;
+ border-bottom: 1px solid #ccc;
+ background-color: #ddd;
+ display: flex;
+ padding: 5px;
+ flex: 0 0 auto;
+ }
+ title {
+ display: inline;
+ font-weight: bold;
+ flex: 1 1 auto;
+ }
+ close-button {
+ -webkit-align-self: flex-end;
+ border: 1px solid #eee;
+ background-color: #999;
+ font-size: 10pt;
+ font-weight: bold;
+ padding: 2px;
+ text-align: center;
+ width: 16px;
+ }
+ close-button:hover {
+ background-color: #ddd;
+ border-color: black;
+ cursor: pointer;
+ }
+ overlay-content {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ overflow-y: auto;
+ padding: 10px;
+ min-width: 300px;
+ min-height: 0;
+ }
+ button-bar {
+ -webkit-align-items: baseline;
+ border-top: 1px solid #ccc;
+ display: flex;
+ flex: 0 0 auto;
+ flex-direction: row-reverse;
+ padding: 4px;
+ }
+ </style>
+
+ <overlay-mask>
+ <overlay-vertical-centering-container>
+ <overlay-frame>
+ <title-bar>
+ <title></title>
+ <close-button>&#x2715</close-button>
+ </title-bar>
+ <overlay-content>
+ <content></content>
+ </overlay-content>
+ <button-bar></button-bar>
+ </overlay-frame>
+ </overlay-vertical-centering-container>
+ </overlay-mask>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Implements an element that is hidden by default, but
+ * when shown, dims and (attempts to) disable the main document.
+ *
+ * You can turn any div into an overlay. Note that while an
+ * overlay element is shown, its parent is changed. Hiding the overlay
+ * restores its original parentage.
+ *
+ */
+tr.exportTo('tr.ui.b', function() {
+ if (tr.isHeadless) return {};
+
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ /**
+ * Creates a new overlay element. It will not be visible until shown.
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ const Overlay = tr.ui.b.define('overlay');
+
+ Overlay.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ /**
+ * Initializes the overlay element.
+ */
+ decorate() {
+ Polymer.dom(this).classList.add('overlay');
+
+ this.parentEl_ = this.ownerDocument.body;
+
+ this.visible_ = false;
+ this.userCanClose_ = true;
+
+ this.onKeyDown_ = this.onKeyDown_.bind(this);
+ this.onClick_ = this.onClick_.bind(this);
+ this.onFocusIn_ = this.onFocusIn_.bind(this);
+ this.onDocumentClick_ = this.onDocumentClick_.bind(this);
+ this.onClose_ = this.onClose_.bind(this);
+
+ this.addEventListener('visible-change',
+ tr.ui.b.Overlay.prototype.onVisibleChange_.bind(this), true);
+
+ // Setup the shadow root
+ const createShadowRoot = this.createShadowRoot ||
+ this.webkitCreateShadowRoot;
+ this.shadow_ = createShadowRoot.call(this);
+ Polymer.dom(this.shadow_).appendChild(
+ tr.ui.b.instantiateTemplate('#overlay-template', THIS_DOC));
+
+ this.closeBtn_ = Polymer.dom(this.shadow_).querySelector('close-button');
+ this.closeBtn_.addEventListener('click', this.onClose_);
+
+ Polymer.dom(this.shadow_)
+ .querySelector('overlay-frame')
+ .addEventListener('click', this.onClick_);
+
+ this.observer_ = new WebKitMutationObserver(
+ this.didButtonBarMutate_.bind(this));
+ this.observer_.observe(
+ Polymer.dom(this.shadow_).querySelector('button-bar'),
+ { childList: true });
+
+ // title is a variable on regular HTMLElements. However, we want to
+ // use it for something more useful.
+ Object.defineProperty(
+ this, 'title', {
+ get() {
+ return Polymer.dom(Polymer.dom(this.shadow_)
+ .querySelector('title')).textContent;
+ },
+ set(title) {
+ Polymer.dom(Polymer.dom(this.shadow_).querySelector('title'))
+ .textContent = title;
+ }
+ });
+ },
+
+ set userCanClose(userCanClose) {
+ this.userCanClose_ = userCanClose;
+ this.closeBtn_.style.display =
+ userCanClose ? 'block' : 'none';
+ },
+
+ get buttons() {
+ return Polymer.dom(this.shadow_).querySelector('button-bar');
+ },
+
+ get visible() {
+ return this.visible_;
+ },
+
+ set visible(newValue) {
+ if (this.visible_ === newValue) return;
+
+ this.visible_ = newValue;
+ const e = new tr.b.Event('visible-change');
+ this.dispatchEvent(e);
+ },
+
+ onVisibleChange_() {
+ this.visible_ ? this.show_() : this.hide_();
+ },
+
+ show_() {
+ Polymer.dom(this.parentEl_).appendChild(this);
+
+ if (this.userCanClose_) {
+ this.addEventListener('keydown', this.onKeyDown_.bind(this));
+ this.addEventListener('click', this.onDocumentClick_.bind(this));
+ this.closeBtn_.addEventListener('click', this.onClose_);
+ }
+
+ this.parentEl_.addEventListener('focusin', this.onFocusIn_);
+ this.tabIndex = 0;
+
+ // Focus the first thing we find that makes sense. (Skip the close button
+ // as it doesn't make sense as the first thing to focus.)
+ const elList =
+ Polymer.dom(this).querySelectorAll('button, input, list, select, a');
+ if (elList.length > 0) {
+ if (elList[0] === this.closeBtn_) {
+ if (elList.length > 1) return elList[1].focus();
+ } else {
+ return elList[0].focus();
+ }
+ }
+ this.focus();
+ },
+
+ hide_() {
+ Polymer.dom(this.parentEl_).removeChild(this);
+
+ this.parentEl_.removeEventListener('focusin', this.onFocusIn_);
+
+ if (this.closeBtn_) {
+ this.closeBtn_.removeEventListener('click', this.onClose_);
+ }
+
+ document.removeEventListener('keydown', this.onKeyDown_);
+ document.removeEventListener('click', this.onDocumentClick_);
+ },
+
+ onClose_(e) {
+ this.visible = false;
+ if ((e.type !== 'keydown') ||
+ (e.type === 'keydown' && e.keyCode === 27)) {
+ e.stopPropagation();
+ }
+ e.preventDefault();
+ tr.b.dispatchSimpleEvent(this, 'closeclick');
+ },
+
+ onFocusIn_(e) {
+ // Prevent focus from leaving the overlay.
+
+ let node = e.target;
+ while (node) {
+ if (node === this) {
+ // |this| contains |e.target|, so nothing needs to be done. Allow
+ // focus to move from |this| to |e.target|.
+ return;
+ }
+ node = node.parentNode;
+ }
+
+ // |e.target| is outside of |this|, so focus |this|.
+ tr.b.timeout(0).then(() => this.focus());
+ e.preventDefault();
+ e.stopPropagation();
+ },
+
+ didButtonBarMutate_(e) {
+ const hasButtons = this.buttons.children.length > 0;
+ if (hasButtons) {
+ Polymer.dom(this.shadow_).querySelector('button-bar').style.display =
+ undefined;
+ } else {
+ Polymer.dom(this.shadow_).querySelector('button-bar').style.display =
+ 'none';
+ }
+ },
+
+ onKeyDown_(e) {
+ // Disallow shift-tab back to another element.
+ if (e.keyCode === 9 && // tab
+ e.shiftKey &&
+ e.target === this) {
+ e.preventDefault();
+ return;
+ }
+
+ if (e.keyCode !== 27) return; // escape
+
+ this.onClose_(e);
+ },
+
+ onClick_(e) {
+ e.stopPropagation();
+ },
+
+ onDocumentClick_(e) {
+ if (!this.userCanClose_) return;
+
+ this.onClose_(e);
+ }
+ };
+
+ Overlay.showError = function(msg, opt_err) {
+ const o = new Overlay();
+ o.title = 'Error';
+ Polymer.dom(o).textContent = msg;
+ if (opt_err) {
+ const e = tr.b.normalizeException(opt_err);
+
+ const stackDiv = document.createElement('pre');
+ Polymer.dom(stackDiv).textContent = e.stack;
+ stackDiv.style.paddingLeft = '8px';
+ stackDiv.style.margin = 0;
+ Polymer.dom(o).appendChild(stackDiv);
+ }
+ const b = document.createElement('button');
+ Polymer.dom(b).textContent = 'OK';
+ b.addEventListener('click', function() {
+ o.visible = false;
+ });
+ Polymer.dom(o.buttons).appendChild(b);
+ o.visible = true;
+ return o;
+ };
+
+ return {
+ Overlay,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html
new file mode 100644
index 00000000000..ac22b08aa6b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/overlay_test.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function addShowButtonForDialog(dlg) {
+ const btn = document.createElement('button');
+ Polymer.dom(btn).textContent = 'Launch Overlay';
+ btn.addEventListener('click', function(e) {
+ dlg.visible = true;
+ e.stopPropagation();
+ });
+
+ this.addHTMLOutput(btn);
+ }
+
+ function makeButton(title) {
+ const btn = document.createElement('button');
+ Polymer.dom(btn).textContent = title;
+ return btn;
+ }
+
+ function makeCloseButton(dlg) {
+ const btn = makeButton('close');
+ btn.addEventListener('click', function(e) {
+ dlg.onClose_(e);
+ });
+ return btn;
+ }
+
+ test('instantiate', function() {
+ const dlg = new tr.ui.b.Overlay();
+ Polymer.dom(dlg).classList.add('example-overlay');
+ dlg.title = 'ExampleOverlay';
+ Polymer.dom(dlg).innerHTML = 'hello';
+ Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button'));
+ Polymer.dom(dlg.buttons).appendChild(makeCloseButton(dlg));
+ Polymer.dom(dlg.buttons).appendChild(tr.ui.b.createSpan(
+ {textContent: 'i am a span'}));
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('instantiate_noButtons', function() {
+ const dlg = new tr.ui.b.Overlay();
+ Polymer.dom(dlg).classList.add('example-overlay');
+ dlg.title = 'ExampleOverlay';
+ Polymer.dom(dlg).innerHTML = 'hello';
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('instantiate_disableUserClose', function() {
+ const dlg = new tr.ui.b.Overlay();
+ Polymer.dom(dlg).classList.add('example-overlay');
+ dlg.userCanClose = false;
+ dlg.title = 'Unclosable';
+ Polymer.dom(dlg).innerHTML = 'This has no close X button.';
+ Polymer.dom(dlg.buttons).appendChild(makeCloseButton(dlg));
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('instantiateTall', function() {
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'TallContent';
+ const contentEl = document.createElement('div');
+ contentEl.style.overflowY = 'auto';
+ Polymer.dom(dlg).appendChild(contentEl);
+
+ for (let i = 0; i < 1000; i++) {
+ const el = document.createElement('div');
+ Polymer.dom(el).textContent = 'line ' + i;
+ Polymer.dom(contentEl).appendChild(el);
+ }
+
+
+ Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button'));
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('instantiateTallWithManyDirectChildren', function() {
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'TallContent';
+ for (let i = 0; i < 100; i++) {
+ const el = document.createElement('div');
+ el.style.webkitFlex = '1 0 auto';
+ Polymer.dom(el).textContent = 'line ' + i;
+ Polymer.dom(dlg).appendChild(el);
+ }
+
+ Polymer.dom(dlg.buttons).appendChild(makeButton('i am a button'));
+ addShowButtonForDialog.call(this, dlg);
+ });
+
+ test('closeclickEvent', function() {
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'Test closeclick event';
+ const closeBtn = makeCloseButton(dlg);
+ Polymer.dom(dlg.buttons).appendChild(closeBtn);
+
+ let closeClicked = false;
+ dlg.addEventListener('closeclick', function() {
+ closeClicked = true;
+ });
+
+ dlg.visible = true;
+ return tr.b.timeout(60).then(() => {
+ closeBtn.click();
+ assert.isTrue(closeClicked);
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html
new file mode 100644
index 00000000000..8a8016142ab
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_postload.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<script>
+'use strict';
+
+if (!Polymer.Settings.useNativeShadow) {
+ tr.showPanic('Polymer error', 'base only works in shadow mode');
+}
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html
new file mode 100644
index 00000000000..4c21ef991ca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/polymer_preload.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<script>
+'use strict';
+
+// Force Polymer into native shadowDom mode
+if (window.Polymer) {
+ throw new Error('Cannot proceed. Polymer already present.');
+}
+window.Polymer = {};
+window.Polymer.dom = 'shadow';
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html
new file mode 100644
index 00000000000..d3d91fa6c00
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/quad_stack_view.html
@@ -0,0 +1,688 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/math/bbox2.html">
+<link rel="import" href="/tracing/base/math/math.html">
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/ui/base/camera.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<link rel="import" href="/tracing/ui/base/mouse_tracker.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<template id="quad-stack-view-template">
+ <style>
+ #chrome-left {
+ background-image: url('../images/chrome-left.png');
+ display: none;
+ }
+ #chrome-mid {
+ background-image: url('../images/chrome-mid.png');
+ display: none;
+ }
+ #chrome-right {
+ background-image: url('../images/chrome-right.png');
+ display: none;
+ }
+ </style>
+
+ <div id="header"></div>
+ <input id="stacking-distance-slider" type="range" min=1 max=400 step=1>
+ </input>
+ <div id="canvas-scroller">
+ <canvas id="canvas"></canvas>
+ </div>
+ <img id="chrome-left"/>
+ <img id="chrome-mid"/>
+ <img id="chrome-right"/>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview QuadStackView controls the content and viewing angle a
+ * QuadStack.
+ */
+tr.exportTo('tr.ui.b', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ const constants = {};
+ constants.IMAGE_LOAD_RETRY_TIME_MS = 500;
+ constants.SUBDIVISION_MINIMUM = 1;
+ constants.SUBDIVISION_RECURSION_DEPTH = 3;
+ constants.SUBDIVISION_DEPTH_THRESHOLD = 100;
+ constants.FAR_PLANE_DISTANCE = 10000;
+
+ // Care of bckenney@ via
+ // http://extremelysatisfactorytotalitarianism.com/blog/?p=2120
+ function drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2) {
+ const tmpP0 = [p0[0], p0[1]];
+ const tmpP1 = [p1[0], p1[1]];
+ const tmpP2 = [p2[0], p2[1]];
+ const tmpT0 = [t0[0], t0[1]];
+ const tmpT1 = [t1[0], t1[1]];
+ const tmpT2 = [t2[0], t2[1]];
+
+ ctx.beginPath();
+ ctx.moveTo(tmpP0[0], tmpP0[1]);
+ ctx.lineTo(tmpP1[0], tmpP1[1]);
+ ctx.lineTo(tmpP2[0], tmpP2[1]);
+ ctx.closePath();
+
+ tmpP1[0] -= tmpP0[0];
+ tmpP1[1] -= tmpP0[1];
+ tmpP2[0] -= tmpP0[0];
+ tmpP2[1] -= tmpP0[1];
+
+ tmpT1[0] -= tmpT0[0];
+ tmpT1[1] -= tmpT0[1];
+ tmpT2[0] -= tmpT0[0];
+ tmpT2[1] -= tmpT0[1];
+
+ const det = 1 / (tmpT1[0] * tmpT2[1] - tmpT2[0] * tmpT1[1]);
+
+ // linear transformation
+ const a = (tmpT2[1] * tmpP1[0] - tmpT1[1] * tmpP2[0]) * det;
+ const b = (tmpT2[1] * tmpP1[1] - tmpT1[1] * tmpP2[1]) * det;
+ const c = (tmpT1[0] * tmpP2[0] - tmpT2[0] * tmpP1[0]) * det;
+ const d = (tmpT1[0] * tmpP2[1] - tmpT2[0] * tmpP1[1]) * det;
+
+ // translation
+ const e = tmpP0[0] - a * tmpT0[0] - c * tmpT0[1];
+ const f = tmpP0[1] - b * tmpT0[0] - d * tmpT0[1];
+
+ ctx.save();
+ ctx.transform(a, b, c, d, e, f);
+ ctx.clip();
+ ctx.drawImage(img, 0, 0);
+ ctx.restore();
+ }
+
+ function drawTriangleSub(
+ ctx, img, p0, p1, p2, t0, t1, t2, opt_recursionDepth) {
+ const depth = opt_recursionDepth || 0;
+
+ // We may subdivide if we are not at the limit of recursion.
+ let subdivisionIndex = 0;
+ if (depth < constants.SUBDIVISION_MINIMUM) {
+ subdivisionIndex = 7;
+ } else if (depth < constants.SUBDIVISION_RECURSION_DEPTH) {
+ if (Math.abs(p0[2] - p1[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) {
+ subdivisionIndex += 1;
+ }
+ if (Math.abs(p0[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) {
+ subdivisionIndex += 2;
+ }
+ if (Math.abs(p1[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD) {
+ subdivisionIndex += 4;
+ }
+ }
+
+ // These need to be created every time, since temporaries
+ // outside of the scope will be rewritten in recursion.
+ const p01 = vec4.create();
+ const p02 = vec4.create();
+ const p12 = vec4.create();
+ const t01 = vec2.create();
+ const t02 = vec2.create();
+ const t12 = vec2.create();
+
+ // Calculate the position before w-divide.
+ for (let i = 0; i < 2; ++i) {
+ p0[i] *= p0[2];
+ p1[i] *= p1[2];
+ p2[i] *= p2[2];
+ }
+
+ // Interpolate the 3d position.
+ for (let i = 0; i < 4; ++i) {
+ p01[i] = (p0[i] + p1[i]) / 2;
+ p02[i] = (p0[i] + p2[i]) / 2;
+ p12[i] = (p1[i] + p2[i]) / 2;
+ }
+
+ // Re-apply w-divide to the original points and the interpolated ones.
+ for (let i = 0; i < 2; ++i) {
+ p0[i] /= p0[2];
+ p1[i] /= p1[2];
+ p2[i] /= p2[2];
+
+ p01[i] /= p01[2];
+ p02[i] /= p02[2];
+ p12[i] /= p12[2];
+ }
+
+ // Interpolate the texture coordinates.
+ for (let i = 0; i < 2; ++i) {
+ t01[i] = (t0[i] + t1[i]) / 2;
+ t02[i] = (t0[i] + t2[i]) / 2;
+ t12[i] = (t1[i] + t2[i]) / 2;
+ }
+
+ // Based on the index, we subdivide the triangle differently.
+ // Assuming the triangle is p0, p1, p2 and points between i j
+ // are represented as pij (that is, a point between p2 and p0
+ // is p02, etc), then the new triangles are defined by
+ // the 3rd 4th and 5th arguments into the function.
+ switch (subdivisionIndex) {
+ case 1:
+ drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
+ drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
+ break;
+ case 2:
+ drawTriangleSub(ctx, img, p0, p1, p02, t0, t1, t02, depth + 1);
+ drawTriangleSub(ctx, img, p1, p02, p2, t1, t02, t2, depth + 1);
+ break;
+ case 3:
+ drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
+ drawTriangleSub(ctx, img, p02, p01, p2, t02, t01, t2, depth + 1);
+ drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
+ break;
+ case 4:
+ drawTriangleSub(ctx, img, p0, p12, p2, t0, t12, t2, depth + 1);
+ drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
+ break;
+ case 5:
+ drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
+ drawTriangleSub(ctx, img, p2, p01, p12, t2, t01, t12, depth + 1);
+ drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
+ break;
+ case 6:
+ drawTriangleSub(ctx, img, p0, p12, p02, t0, t12, t02, depth + 1);
+ drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
+ drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
+ break;
+ case 7:
+ drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
+ drawTriangleSub(ctx, img, p01, p12, p02, t01, t12, t02, depth + 1);
+ drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
+ drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
+ break;
+ default:
+ // In the 0 case and all other cases, we simply draw the triangle.
+ drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2);
+ break;
+ }
+ }
+
+ // Created to avoid creating garbage when doing bulk transforms.
+ const tmpVec4 = vec4.create();
+ function transform(transformed, point, matrix, viewport) {
+ vec4.set(tmpVec4, point[0], point[1], 0, 1);
+ vec4.transformMat4(tmpVec4, tmpVec4, matrix);
+
+ let w = tmpVec4[3];
+ if (w < 1e-6) w = 1e-6;
+
+ transformed[0] = ((tmpVec4[0] / w) + 1) * viewport.width / 2;
+ transformed[1] = ((tmpVec4[1] / w) + 1) * viewport.height / 2;
+ transformed[2] = w;
+ }
+
+ function drawProjectedQuadBackgroundToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas) {
+ if (quad.imageData) {
+ quadCanvas.width = quad.imageData.width;
+ quadCanvas.height = quad.imageData.height;
+ quadCanvas.getContext('2d').putImageData(quad.imageData, 0, 0);
+ const quadBBox = new tr.b.math.BBox2();
+ quadBBox.addQuad(quad);
+ const iw = quadCanvas.width;
+ const ih = quadCanvas.height;
+ drawTriangleSub(
+ ctx, quadCanvas,
+ p1, p2, p4,
+ [0, 0], [iw, 0], [0, ih]);
+ drawTriangleSub(
+ ctx, quadCanvas,
+ p2, p3, p4,
+ [iw, 0], [iw, ih], [0, ih]);
+ }
+
+ if (quad.backgroundColor) {
+ ctx.fillStyle = quad.backgroundColor;
+ ctx.beginPath();
+ ctx.moveTo(p1[0], p1[1]);
+ ctx.lineTo(p2[0], p2[1]);
+ ctx.lineTo(p3[0], p3[1]);
+ ctx.lineTo(p4[0], p4[1]);
+ ctx.closePath();
+ ctx.fill();
+ }
+ }
+
+ function drawProjectedQuadOutlineToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas) {
+ ctx.beginPath();
+ ctx.moveTo(p1[0], p1[1]);
+ ctx.lineTo(p2[0], p2[1]);
+ ctx.lineTo(p3[0], p3[1]);
+ ctx.lineTo(p4[0], p4[1]);
+ ctx.closePath();
+ ctx.save();
+ if (quad.borderColor) {
+ ctx.strokeStyle = quad.borderColor;
+ } else {
+ ctx.strokeStyle = 'rgb(128,128,128)';
+ }
+
+ if (quad.shadowOffset) {
+ ctx.shadowColor = 'rgb(0, 0, 0)';
+ ctx.shadowOffsetX = quad.shadowOffset[0];
+ ctx.shadowOffsetY = quad.shadowOffset[1];
+ if (quad.shadowBlur) {
+ ctx.shadowBlur = quad.shadowBlur;
+ }
+ }
+
+ if (quad.borderWidth) {
+ ctx.lineWidth = quad.borderWidth;
+ } else {
+ ctx.lineWidth = 1;
+ }
+
+ ctx.stroke();
+ ctx.restore();
+ }
+
+ function drawProjectedQuadSelectionOutlineToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas) {
+ if (!quad.upperBorderColor) return;
+
+ ctx.lineWidth = 8;
+ ctx.strokeStyle = quad.upperBorderColor;
+
+ ctx.beginPath();
+ ctx.moveTo(p1[0], p1[1]);
+ ctx.lineTo(p2[0], p2[1]);
+ ctx.lineTo(p3[0], p3[1]);
+ ctx.lineTo(p4[0], p4[1]);
+ ctx.closePath();
+ ctx.stroke();
+ }
+
+ function drawProjectedQuadToContext(
+ passNumber, quad, p1, p2, p3, p4, ctx, quadCanvas) {
+ if (passNumber === 0) {
+ drawProjectedQuadBackgroundToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas);
+ } else if (passNumber === 1) {
+ drawProjectedQuadOutlineToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas);
+ } else if (passNumber === 2) {
+ drawProjectedQuadSelectionOutlineToContext(
+ quad, p1, p2, p3, p4, ctx, quadCanvas);
+ } else {
+ throw new Error('Invalid pass number');
+ }
+ }
+
+ const tmpP1 = vec3.create();
+ const tmpP2 = vec3.create();
+ const tmpP3 = vec3.create();
+ const tmpP4 = vec3.create();
+ function transformAndProcessQuads(
+ matrix, viewport, quads, numPasses, handleQuadFunc, opt_arg1, opt_arg2) {
+ for (let passNumber = 0; passNumber < numPasses; passNumber++) {
+ for (let i = 0; i < quads.length; i++) {
+ const quad = quads[i];
+ transform(tmpP1, quad.p1, matrix, viewport);
+ transform(tmpP2, quad.p2, matrix, viewport);
+ transform(tmpP3, quad.p3, matrix, viewport);
+ transform(tmpP4, quad.p4, matrix, viewport);
+ handleQuadFunc(passNumber, quad,
+ tmpP1, tmpP2, tmpP3, tmpP4,
+ opt_arg1, opt_arg2);
+ }
+ }
+ }
+
+ /**
+ * @constructor
+ */
+ const QuadStackView = tr.ui.b.define('quad-stack-view');
+
+ QuadStackView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.className = 'quad-stack-view';
+ this.style.display = 'flex';
+ this.style.position = 'relative';
+
+ const node = tr.ui.b.instantiateTemplate('#quad-stack-view-template',
+ THIS_DOC);
+ Polymer.dom(this).appendChild(node);
+ this.updateHeaderVisibility_();
+ const header = Polymer.dom(this).querySelector('#header');
+ header.style.position = 'absolute';
+ header.style.fontSize = '70%';
+ header.style.top = '10px';
+ header.style.left = '10px';
+ header.style.right = '150px';
+
+ const scroller = Polymer.dom(this).querySelector('#canvas-scroller');
+ scroller.style.flexGrow = 1;
+ scroller.style.flexShrink = 1;
+ scroller.style.flexBasis = 'auto';
+ scroller.style.minWidth = 0;
+ scroller.style.minHeight = 0;
+ scroller.style.overflow = 'auto';
+
+ this.canvas_ = Polymer.dom(this).querySelector('#canvas');
+ this.chromeImages_ = {
+ left: Polymer.dom(this).querySelector('#chrome-left'),
+ mid: Polymer.dom(this).querySelector('#chrome-mid'),
+ right: Polymer.dom(this).querySelector('#chrome-right')
+ };
+
+ const stackingDistanceSlider = Polymer.dom(this).querySelector(
+ '#stacking-distance-slider');
+ stackingDistanceSlider.style.position = 'absolute';
+ stackingDistanceSlider.style.fontSize = '70%';
+ stackingDistanceSlider.style.top = '10px';
+ stackingDistanceSlider.style.right = '10px';
+ stackingDistanceSlider.value = tr.b.Settings.get(
+ 'quadStackView.stackingDistance', 45);
+ stackingDistanceSlider.addEventListener(
+ 'change', this.onStackingDistanceChange_.bind(this));
+ stackingDistanceSlider.addEventListener(
+ 'input', this.onStackingDistanceChange_.bind(this));
+
+ this.trackMouse_();
+
+ this.camera_ = new tr.ui.b.Camera(this.mouseModeSelector_);
+ this.camera_.addEventListener('renderrequired',
+ this.onRenderRequired_.bind(this));
+ this.cameraWasReset_ = false;
+ this.camera_.canvas = this.canvas_;
+
+ this.viewportRect_ = tr.b.math.Rect.fromXYWH(0, 0, 0, 0);
+
+ this.pixelRatio_ = window.devicePixelRatio || 1;
+ },
+
+ updateHeaderVisibility_() {
+ if (this.headerText) {
+ Polymer.dom(this).querySelector('#header').style.display = '';
+ } else {
+ Polymer.dom(this).querySelector('#header').style.display = 'none';
+ }
+ },
+
+ get headerText() {
+ return Polymer.dom(this).querySelector('#header').textContent;
+ },
+
+ set headerText(headerText) {
+ Polymer.dom(this).querySelector('#header').textContent = headerText;
+ this.updateHeaderVisibility_();
+ },
+
+ onStackingDistanceChange_(e) {
+ tr.b.Settings.set('quadStackView.stackingDistance',
+ this.stackingDistance);
+ this.scheduleRender();
+ e.stopPropagation();
+ },
+
+ get stackingDistance() {
+ return Polymer.dom(this).querySelector('#stacking-distance-slider').value;
+ },
+
+ get mouseModeSelector() {
+ return this.mouseModeSelector_;
+ },
+
+ get camera() {
+ return this.camera_;
+ },
+
+ set quads(q) {
+ this.quads_ = q;
+ this.scheduleRender();
+ },
+
+ set deviceRect(rect) {
+ if (!rect || rect.equalTo(this.deviceRect_)) return;
+
+ this.deviceRect_ = rect;
+ this.camera_.deviceRect = rect;
+ this.chromeQuad_ = undefined;
+ },
+
+ resize() {
+ if (!this.offsetParent) return true;
+
+ const width = parseInt(window.getComputedStyle(this.offsetParent).width);
+ const height = parseInt(window.getComputedStyle(
+ this.offsetParent).height);
+ const rect = tr.b.math.Rect.fromXYWH(0, 0, width, height);
+
+ if (rect.equalTo(this.viewportRect_)) return false;
+
+ this.viewportRect_ = rect;
+ this.canvas_.style.width = width + 'px';
+ this.canvas_.style.height = height + 'px';
+ this.canvas_.width = this.pixelRatio_ * width;
+ this.canvas_.height = this.pixelRatio_ * height;
+ if (!this.cameraWasReset_) {
+ this.camera_.resetCamera();
+ this.cameraWasReset_ = true;
+ }
+ return true;
+ },
+
+ readyToDraw() {
+ // If src isn't set yet, set it to ensure we can use
+ // the image to draw onto a canvas.
+ if (!this.chromeImages_.left.src) {
+ let leftContent =
+ window.getComputedStyle(this.chromeImages_.left).backgroundImage;
+ leftContent = tr.ui.b.extractUrlString(leftContent);
+
+ let midContent =
+ window.getComputedStyle(this.chromeImages_.mid).backgroundImage;
+ midContent = tr.ui.b.extractUrlString(midContent);
+
+ let rightContent =
+ window.getComputedStyle(this.chromeImages_.right).backgroundImage;
+ rightContent = tr.ui.b.extractUrlString(rightContent);
+
+ this.chromeImages_.left.src = leftContent;
+ this.chromeImages_.mid.src = midContent;
+ this.chromeImages_.right.src = rightContent;
+ }
+
+ // If all of the images are loaded (height > 0), then
+ // we are ready to draw.
+ return (this.chromeImages_.left.height > 0) &&
+ (this.chromeImages_.mid.height > 0) &&
+ (this.chromeImages_.right.height > 0);
+ },
+
+ get chromeQuad() {
+ if (this.chromeQuad_) return this.chromeQuad_;
+
+ // Draw the chrome border into a separate canvas.
+ const chromeCanvas = document.createElement('canvas');
+ const offsetY = this.chromeImages_.left.height;
+
+ chromeCanvas.width = this.deviceRect_.width;
+ chromeCanvas.height = this.deviceRect_.height + offsetY;
+
+ const leftWidth = this.chromeImages_.left.width;
+ const midWidth = this.chromeImages_.mid.width;
+ const rightWidth = this.chromeImages_.right.width;
+
+ const chromeCtx = chromeCanvas.getContext('2d');
+ chromeCtx.drawImage(this.chromeImages_.left, 0, 0);
+
+ chromeCtx.save();
+ chromeCtx.translate(leftWidth, 0);
+
+ // Calculate the scale of the mid image.
+ const s = (this.deviceRect_.width - leftWidth - rightWidth) / midWidth;
+ chromeCtx.scale(s, 1);
+
+ chromeCtx.drawImage(this.chromeImages_.mid, 0, 0);
+ chromeCtx.restore();
+
+ chromeCtx.drawImage(
+ this.chromeImages_.right, leftWidth + s * midWidth, 0);
+
+ // Construct the quad.
+ const chromeRect = tr.b.math.Rect.fromXYWH(
+ this.deviceRect_.x,
+ this.deviceRect_.y - offsetY,
+ this.deviceRect_.width,
+ this.deviceRect_.height + offsetY);
+ const chromeQuad = tr.b.math.Quad.fromRect(chromeRect);
+ chromeQuad.stackingGroupId = this.maxStackingGroupId_ + 1;
+ chromeQuad.imageData = chromeCtx.getImageData(
+ 0, 0, chromeCanvas.width, chromeCanvas.height);
+ chromeQuad.shadowOffset = [0, 0];
+ chromeQuad.shadowBlur = 5;
+ chromeQuad.borderWidth = 3;
+ this.chromeQuad_ = chromeQuad;
+ return this.chromeQuad_;
+ },
+
+ scheduleRender() {
+ if (this.redrawScheduled_) return false;
+ this.redrawScheduled_ = true;
+ tr.b.requestAnimationFrame(this.render, this);
+ },
+
+ onRenderRequired_(e) {
+ this.scheduleRender();
+ },
+
+ stackTransformAndProcessQuads_(
+ numPasses, handleQuadFunc, includeChromeQuad, opt_arg1, opt_arg2) {
+ const mv = this.camera_.modelViewMatrix;
+ const p = this.camera_.projectionMatrix;
+
+ const viewport = tr.b.math.Rect.fromXYWH(
+ 0, 0, this.canvas_.width, this.canvas_.height);
+
+ // Calculate the quad stacks.
+ const quadStacks = [];
+ for (let i = 0; i < this.quads_.length; ++i) {
+ const quad = this.quads_[i];
+ const stackingId = quad.stackingGroupId || 0;
+ while (stackingId >= quadStacks.length) {
+ quadStacks.push([]);
+ }
+
+ quadStacks[stackingId].push(quad);
+ }
+
+ const mvp = mat4.create();
+ this.maxStackingGroupId_ = quadStacks.length;
+ const effectiveStackingDistance =
+ this.stackingDistance * this.camera_.stackingDistanceDampening;
+
+ // Draw the quad stacks, raising each subsequent level.
+ mat4.multiply(mvp, p, mv);
+ for (let i = 0; i < quadStacks.length; ++i) {
+ transformAndProcessQuads(mvp, viewport, quadStacks[i],
+ numPasses, handleQuadFunc,
+ opt_arg1, opt_arg2);
+
+ mat4.translate(mv, mv, [0, 0, effectiveStackingDistance]);
+ mat4.multiply(mvp, p, mv);
+ }
+
+ if (includeChromeQuad && this.deviceRect_) {
+ transformAndProcessQuads(mvp, viewport, [this.chromeQuad],
+ numPasses, drawProjectedQuadToContext,
+ opt_arg1, opt_arg2);
+ }
+ },
+
+ render() {
+ this.redrawScheduled_ = false;
+
+ if (!this.readyToDraw()) {
+ setTimeout(this.scheduleRender.bind(this),
+ constants.IMAGE_LOAD_RETRY_TIME_MS);
+ return;
+ }
+
+ if (!this.quads_) return;
+
+ const canvasCtx = this.canvas_.getContext('2d');
+ if (!this.resize()) {
+ canvasCtx.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+ }
+
+ const quadCanvas = document.createElement('canvas');
+ this.stackTransformAndProcessQuads_(
+ 3, drawProjectedQuadToContext, true,
+ canvasCtx, quadCanvas);
+ quadCanvas.width = 0; // Hack: Frees the quadCanvas' resources.
+ },
+
+ trackMouse_() {
+ this.mouseModeSelector_ = document.createElement(
+ 'tr-ui-b-mouse-mode-selector');
+ this.mouseModeSelector_.targetElement = this.canvas_;
+ this.mouseModeSelector_.supportedModeMask =
+ tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION |
+ tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN |
+ tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM |
+ tr.ui.b.MOUSE_SELECTOR_MODE.ROTATE;
+ this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN;
+ this.mouseModeSelector_.pos = {x: 0, y: 100};
+ Polymer.dom(this).appendChild(this.mouseModeSelector_);
+ this.mouseModeSelector_.settingsKey =
+ 'quadStackView.mouseModeSelector';
+
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ tr.ui.b.MOUSE_SELECTOR_MODE.ROTATE, tr.ui.b.MODIFIER.SHIFT);
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN, tr.ui.b.MODIFIER.SPACE);
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM, tr.ui.b.MODIFIER.CMD_OR_CTRL);
+
+ this.mouseModeSelector_.addEventListener('updateselection',
+ this.onSelectionUpdate_.bind(this));
+ this.mouseModeSelector_.addEventListener('endselection',
+ this.onSelectionUpdate_.bind(this));
+ },
+
+ extractRelativeMousePosition_(e) {
+ const br = this.canvas_.getBoundingClientRect();
+ return [
+ this.pixelRatio_ * (e.clientX - this.canvas_.offsetLeft - br.left),
+ this.pixelRatio_ * (e.clientY - this.canvas_.offsetTop - br.top)
+ ];
+ },
+
+ onSelectionUpdate_(e) {
+ const mousePos = this.extractRelativeMousePosition_(e);
+ const res = [];
+ function handleQuad(passNumber, quad, p1, p2, p3, p4) {
+ if (tr.b.math.pointInImplicitQuad(mousePos, p1, p2, p3, p4)) {
+ res.push(quad);
+ }
+ }
+ this.stackTransformAndProcessQuads_(1, handleQuad, false);
+ e = new tr.b.Event('selectionchange');
+ e.quads = res;
+ this.dispatchEvent(e);
+ }
+ };
+
+ return {
+ QuadStackView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html
new file mode 100644
index 00000000000..27edfb59152
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tr-ui-b-radio-picker'>
+ <template>
+ <style>
+ :host([vertical]) #container {
+ flex-direction: column;
+ }
+ :host(:not[vertical]) #container {
+ flex-direction: row;
+ }
+ #container {
+ display: flex;
+ }
+ #container > div {
+ padding-left: 1em;
+ padding-bottom: 0.5em;
+ }
+ </style>
+ <div id="container"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-radio-picker',
+
+ created() {
+ this.needsInit_ = true;
+ this.settingsKey_ = undefined;
+ this.isReady_ = false;
+ this.radioButtons_ = undefined;
+ // Keeping track of which key is selected. This member should only be set
+ // set inside select() method to make sure that logical state & the UI
+ // state is consistent.
+ this.selectedKey_ = undefined;
+ },
+
+ ready() {
+ this.isReady_ = true;
+ this.maybeInit_();
+ this.maybeRenderRadioButtons_();
+ },
+
+ get vertical() {
+ return this.getAttribute('vertical');
+ },
+
+ set vertical(vertical) {
+ if (vertical) {
+ this.setAttribute('vertical', true);
+ } else {
+ this.removeAttribute('vertical');
+ }
+ },
+
+ get settingsKey() {
+ return this.settingsKey_;
+ },
+
+ set settingsKey(settingsKey) {
+ if (!this.needsInit_) {
+ throw new Error('Already initialized.');
+ }
+ this.settingsKey_ = settingsKey;
+ this.maybeInit_();
+ },
+
+ maybeInit_() {
+ if (!this.needsInit_) return;
+ if (this.settingsKey_ === undefined) return;
+ this.needsInit_ = false;
+ this.select(tr.b.Settings.get(this.settingsKey_));
+ },
+
+ set items(items) {
+ this.radioButtons_ = {};
+ items.forEach(function(e) {
+ if (e.key in this.radioButtons_) {
+ throw new Error(e.key + ' already exists');
+ }
+ const radioButton = document.createElement('div');
+ const input = document.createElement('input');
+ const label = document.createElement('label');
+ input.type = 'radio';
+ input.id = e.label;
+ input.addEventListener('click', function() {
+ this.select(e.key);
+ }.bind(this));
+ Polymer.dom(label).innerHTML = e.label;
+ label.htmlFor = e.label;
+ label.style.display = 'inline';
+ Polymer.dom(radioButton).appendChild(input);
+ Polymer.dom(radioButton).appendChild(label);
+ this.radioButtons_[e.key] = input;
+ }.bind(this));
+
+ this.maybeInit_();
+ this.maybeRenderRadioButtons_();
+ },
+
+ maybeRenderRadioButtons_() {
+ if (!this.isReady_) return;
+ if (this.radioButtons_ === undefined) return;
+ for (const key in this.radioButtons_) {
+ Polymer.dom(this.$.container).appendChild(
+ this.radioButtons_[key].parentElement);
+ }
+ if (this.selectedKey_ !== undefined) {
+ this.select(this.selectedKey_);
+ }
+ },
+
+ select(key) {
+ if (key === undefined || key === this.selectedKey_) {
+ return;
+ }
+ if (this.radioButtons_ === undefined) {
+ this.selectedKey_ = key;
+ return;
+ }
+ if (!(key in this.radioButtons_)) {
+ throw new Error(key + ' does not exists');
+ }
+ // Unselect the previous radio, update the key & select the new one.
+ if (this.selectedKey_ !== undefined) {
+ this.radioButtons_[this.selectedKey_].checked = false;
+ }
+ this.selectedKey_ = key;
+ tr.b.Settings.set(this.settingsKey_, this.selectedKey_);
+ if (this.selectedKey_ !== undefined) {
+ this.radioButtons_[this.selectedKey_].checked = true;
+ }
+
+ this.dispatchEvent(new tr.b.Event('change', false));
+ },
+
+ get selectedKey() {
+ return this.selectedKey_;
+ },
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html
new file mode 100644
index 00000000000..292cfaa4c9b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/radio_picker_test.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/radio_picker.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ this.addHTMLOutput(rp);
+ assert.strictEqual(rp.selectedKey, undefined);
+ rp.select('Toyota');
+ assert.strictEqual(rp.selectedKey, 'Toyota');
+ });
+
+ test('persistentState_setSelectedKeyAfterSettingItems', function() {
+ const items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ const container1 = tr.ui.b.createDiv({textContent: 'Radio Picker One'});
+ container1.style.border = 'solid';
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.items = items;
+ rp.settingsKey = 'radio-picker-test-one';
+ Polymer.dom(container1).appendChild(rp);
+ this.addHTMLOutput(container1);
+ assert.strictEqual(rp.selectedKey, undefined);
+ rp.select('Toyota');
+ assert.strictEqual(rp.selectedKey, 'Toyota');
+
+ const container2 = tr.ui.b.createDiv({
+ textContent: 'Radio Picker Two (same settingKey as Radio Picker One)'});
+ container2.style.border = 'solid';
+ const rp2 = document.createElement('tr-ui-b-radio-picker');
+ rp2.items = items;
+ rp2.settingsKey = 'radio-picker-test-one';
+ Polymer.dom(container2).appendChild(rp2);
+ this.addHTMLOutput(container2);
+
+ assert.strictEqual(rp2.selectedKey, 'Toyota');
+ });
+
+ test('persistentState_setSelectedKeyBeforeSettingItems', function() {
+ const items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ const container1 = tr.ui.b.createDiv({textContent: 'Radio Picker One'});
+ container1.style.border = 'solid';
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.settingsKey = 'radio-picker-test-two';
+ rp.items = items;
+ Polymer.dom(container1).appendChild(rp);
+ this.addHTMLOutput(container1);
+ assert.strictEqual(rp.selectedKey, undefined);
+ rp.select('Boeing');
+ assert.strictEqual(rp.selectedKey, 'Boeing');
+
+ const container2 = tr.ui.b.createDiv({
+ textContent: 'Radio Picker Two (same settingKey as Radio Picker One)'});
+ container2.style.border = 'solid';
+ const rp2 = document.createElement('tr-ui-b-radio-picker');
+ rp2.settingsKey = 'radio-picker-test-two';
+ Polymer.dom(container2).appendChild(rp2);
+ this.addHTMLOutput(container2);
+ rp2.items = items;
+
+ assert.strictEqual(rp2.selectedKey, 'Boeing');
+ });
+
+ test('changeEventFired', function() {
+ const items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.items = items;
+ this.addHTMLOutput(rp);
+ rp.select('Boeing');
+ assert.strictEqual(rp.selectedKey, 'Boeing');
+ let fired = false;
+ rp.addEventListener('change', function(e) {
+ fired = true;
+ assert.strictEqual('Toyota', e.target.selectedKey);
+ });
+ rp.select('Toyota');
+ assert.isTrue(fired);
+ });
+
+ test('verticalAttribute', function() {
+ const items = [
+ {key: 'Toyota', label: 'I want to drive Toyota'},
+ {key: 'Boeing', label: 'I want to fly'},
+ {key: 'Submarine', label: 'I want to swim'}
+ ];
+ const rp = document.createElement('tr-ui-b-radio-picker');
+ rp.items = items;
+ this.addHTMLOutput(rp);
+ assert.isNull(rp.getAttribute('vertical'));
+ rp.vertical = true;
+ assert.strictEqual(rp.getAttribute('vertical'), 'true');
+ rp.vertical = false;
+ assert.isNull(rp.getAttribute('vertical'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html
new file mode 100644
index 00000000000..38b292e2360
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/ui/base/chart_base_2d.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const ScatterChart = tr.ui.b.define('scatter-chart', tr.ui.b.ChartBase2D);
+
+ // @constructor
+ ScatterChart.Dot = function(x, y, radius, color, breadcrumb) {
+ this.x = x;
+ this.y = y;
+ this.radius = radius;
+ this.color = color;
+ this.breadcrumb = breadcrumb;
+ };
+
+ ScatterChart.prototype = {
+ __proto__: tr.ui.b.ChartBase2D.prototype,
+
+ decorate() {
+ super.decorate();
+ this.brushedXRange_ = new tr.b.math.Range();
+ this.brushedYRange_ = new tr.b.math.Range();
+ },
+
+ get hideLegend() {
+ return true;
+ },
+
+ get defaultGraphHeight() {
+ return 100;
+ },
+
+ get defaultGraphWidth() {
+ return 100;
+ },
+
+ updateMargins_() {
+ super.updateMargins_();
+ if (this.data.length === 0) return;
+
+ const rightOverhangPx = tr.b.math.Statistics.max(
+ this.data, d => this.xScale_(d.x) + d.radius - this.graphWidth);
+ this.margin.right = Math.max(this.margin.right, rightOverhangPx);
+
+ const topOverhangPx = tr.b.math.Statistics.max(
+ this.data, d => (this.graphHeight - this.yScale_(d.y)) + d.radius) -
+ this.graphHeight;
+ this.margin.top = Math.max(this.margin.top, topOverhangPx);
+ },
+
+ setBrushedRanges(xRange, yRange) {
+ this.brushedXRange_.reset();
+ this.brushedYRange_.reset();
+ this.brushedXRange_.addRange(xRange);
+ this.brushedYRange_.addRange(yRange);
+ this.updateContents_();
+ },
+
+ updateBrushContents_(brushSel) {
+ brushSel.selectAll('*').remove();
+ if (this.brushedXRange_.isEmpty || this.brushedYRange_.isEmpty) return;
+
+ const brushRectsSel = brushSel.selectAll('rect').data([undefined]);
+ brushRectsSel.enter().append('rect')
+ .attr('x', () => this.xScale_(this.brushedXRange_.min))
+ .attr('y', () => this.yScale_(this.brushedYRange_.max))
+ .attr('width', () => this.xScale_(this.brushedXRange_.max) -
+ this.xScale_(this.brushedXRange_.min))
+ .attr('height', () => this.yScale_(this.brushedYRange_.min) -
+ this.yScale_(this.brushedYRange_.max));
+ brushRectsSel.exit().remove();
+ },
+
+ setDataFromCallbacks(data, getX, getY, getRadius, getColor) {
+ this.data = data.map(d => new ScatterChart.Dot(
+ getX(d), getY(d), getRadius(d), getColor(d), d));
+ },
+
+ isDatumFieldSeries_(fieldName) {
+ return fieldName === 'y';
+ },
+
+ updateDataContents_(dataSel) {
+ dataSel.selectAll('*').remove();
+ dataSel.selectAll('circle')
+ .data(this.data_)
+ .enter()
+ .append('circle')
+ .attr('cx', d => this.xScale_(d.x))
+ .attr('cy', d => this.yScale_(d.y))
+ .attr('r', d => d.radius)
+ .attr('fill', d => d.color);
+ }
+ };
+
+ return {
+ ScatterChart,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html
new file mode 100644
index 00000000000..a223589a2ac
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/scatter_chart_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/scatter_chart.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiation_singleSeries', function() {
+ const chart = new tr.ui.b.ScatterChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, y: 100, radius: 2, color: 'red'},
+ {x: 20, y: 110, radius: 20, color: 'blue'},
+ {x: 30, y: 100, radius: 10, color: 'red'},
+ {x: 40, y: 50, radius: 10, color: 'red'}
+ ];
+ });
+
+ test('instantiation_interactiveBrushing', function() {
+ const chart = new tr.ui.b.ScatterChart();
+ this.addHTMLOutput(chart);
+ chart.data = [
+ {x: 10, y: 50, radius: 2, color: 'blue'},
+ {x: 20, y: 60, radius: 3, color: 'red'},
+ {x: 30, y: 80, radius: 4, color: 'orange'},
+ {x: 40, y: 20, radius: 5, color: 'purple'},
+ {x: 50, y: 30, radius: 6, color: 'yellow'},
+ {x: 60, y: 20, radius: 7, color: 'green'},
+ {x: 70, y: 15, radius: 8, color: 'blue'},
+ {x: 80, y: 20, radius: 9, color: 'red'}
+ ];
+
+ let mouseDown = undefined;
+
+ function updateBrushedRange(e) {
+ const xRange = new tr.b.math.Range();
+ if (e.x !== mouseDown.x) {
+ xRange.addValue(mouseDown.x);
+ xRange.addValue(e.x);
+ }
+ const yRange = new tr.b.math.Range();
+ if (e.y !== mouseDown.y) {
+ yRange.addValue(mouseDown.y);
+ yRange.addValue(e.y);
+ }
+ chart.setBrushedRanges(xRange, yRange);
+ }
+
+ chart.addEventListener('item-mousedown', function(e) {
+ mouseDown = e;
+ });
+ chart.addEventListener('item-mousemove', function(e) {
+ updateBrushedRange(e);
+ });
+ chart.addEventListener('item-mouseup', function(e) {
+ updateBrushedRange(e);
+ mouseDown = undefined;
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html
new file mode 100644
index 00000000000..a651fc5cc3d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view.html
@@ -0,0 +1,273 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<!--
+@fileoverview A view that allows the user to control which single tab is
+displayed.
+
+We follow a fairly standard web convention of backing our tabs with hidden radio
+buttons but visible radio button labels (the tabs themselves) which toggle the
+input element when clicked. Using hidden radio buttons makes sense, as both tabs
+and radio buttons are input elements that allow user selection through clicking
+and limit users to having one option selected at a time.
+-->
+<dom-module id='tr-ui-b-tab-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #selection_description, #tabs {
+ font-size: 12px;
+ }
+
+ #selection_description {
+ display: inline-block;
+ font-weight: bold;
+ margin: 9px 0px 4px 20px;
+ }
+
+ #tabs {
+ flex: 0 0 auto;
+ border-top: 1px solid #8e8e8e;
+ border-bottom: 1px solid #8e8e8e;
+ background-color: #ececec;
+ overflow: hidden;
+ margin: 0;
+ }
+
+ #tabs input[type=radio] {
+ display: none;
+ }
+
+ #tabs tab label {
+ cursor: pointer;
+ display: inline-block;
+ border: 1px solid #ececec;
+ margin: 5px 0px 0px 15px;
+ padding: 3px 10px 3px 10px;
+ }
+
+ #tabs tab label span {
+ font-weight: bold;
+ }
+
+ #tabs:focus input[type=radio]:checked ~ label {
+ outline: dotted 1px #8e8e8e;
+ outline-offset: -2px;
+ }
+
+ #tabs input[type=radio]:checked ~ label {
+ background-color: white;
+ border: 1px solid #8e8e8e;
+ border-bottom: 1px solid white;
+ }
+
+ #subView {
+ flex: 1 1 auto;
+ min-width: 0;
+ display: flex;
+ }
+
+ #subView > * {
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+ </style>
+ <div id='tabs' hidden="[[tabsHidden]]">
+ <label id=selection_description>[[label_]]</label>
+ <template is=dom-repeat items=[[subViews_]]>
+ <tab>
+ <input type=radio name=tabs id$=[[computeRadioId_(item)]]
+ on-change='onTabChanged_'
+ checked='[[isChecked_(item)]]'/>
+ <label for$=[[computeRadioId_(item)]]>
+ <template is=dom-if if=[[item.tabIcon]]>
+ <span style$='[[item.tabIcon.style]]'>[[item.tabIcon.text]]</span>
+ </template>
+ [[item.tabLabel]]
+ </label>
+ </tab>
+ </template>
+ </div>
+ <div id='subView'></div>
+ <slot>
+ </slot>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-tab-view',
+
+ properties: {
+ label_: {
+ type: String,
+ value: () => ''
+ },
+ selectedSubView_: Object,
+ subViews_: {
+ type: Array,
+ value: () => []
+ },
+ tabsHidden: {
+ type: Boolean,
+ value: false,
+ observer: 'tabsHiddenChanged_'
+ }
+ },
+
+ ready() {
+ this.$.tabs.addEventListener('keydown', this.onKeyDown_.bind(this), true);
+ this.updateFocusability_();
+ },
+
+ set label(newLabel) {
+ this.set('label_', newLabel);
+ },
+
+ get tabs() {
+ return this.get('subViews_');
+ },
+
+ get selectedSubView() {
+ return this.selectedSubView_;
+ },
+
+ set selectedSubView(subView) {
+ if (subView === this.selectedSubView_) return;
+
+ if (this.selectedSubView_) {
+ Polymer.dom(this.$.subView).removeChild(this.selectedSubView_);
+ const oldInput = this.root.getElementById(this.computeRadioId_(
+ this.selectedSubView_));
+ if (oldInput) {
+ oldInput.checked = false;
+ }
+ }
+
+ this.set('selectedSubView_', subView);
+
+ if (subView) {
+ Polymer.dom(this.$.subView).appendChild(subView);
+ const newInput = this.root.getElementById(this.computeRadioId_(subView));
+ if (newInput) {
+ newInput.checked = true;
+ }
+ }
+
+ this.fire('selected-tab-change');
+ },
+
+ clearSubViews() {
+ this.splice('subViews_', 0, this.subViews_.length);
+ this.selectedSubView = undefined;
+ this.updateFocusability_();
+ },
+
+ addSubView(subView) {
+ this.push('subViews_', subView);
+ if (!this.selectedSubView_) this.selectedSubView = subView;
+
+ this.updateFocusability_();
+ },
+
+ get subViews() {
+ return this.subViews_;
+ },
+
+ resetSubViews(subViews) {
+ this.splice('subViews_', 0, this.subViews_.length);
+ if (subViews.length) {
+ for (const subView of subViews) {
+ this.push('subViews_', subView);
+ }
+ this.selectedSubView = subViews[0];
+ } else {
+ this.selectedSubView = undefined;
+ }
+ this.updateFocusability_();
+ },
+
+ onTabChanged_(event) {
+ this.selectedSubView = event.model.item;
+ },
+
+ isChecked_(subView) {
+ return this.selectedSubView_ === subView;
+ },
+
+ tabsHiddenChanged_() {
+ this.updateFocusability_();
+ },
+
+ onKeyDown_(e) {
+ if (this.tabsHidden) return;
+
+ let keyHandled = false;
+ switch (e.keyCode) {
+ // Arrow left.
+ case 37:
+ keyHandled = this.selectPreviousTabIfPossible();
+ break;
+
+ // Arrow right.
+ case 39:
+ keyHandled = this.selectNextTabIfPossible();
+ break;
+ }
+
+ if (!keyHandled) return;
+ e.stopPropagation();
+ e.preventDefault();
+ },
+
+ selectNextTabIfPossible() {
+ return this.selectTabByOffsetIfPossible_(1);
+ },
+
+ selectPreviousTabIfPossible() {
+ return this.selectTabByOffsetIfPossible_(-1);
+ },
+
+ selectTabByOffsetIfPossible_(offset) {
+ if (!this.selectedSubView_) return false;
+ const currentIndex = this.subViews_.indexOf(this.selectedSubView_);
+ const newSubView = this.tabs[currentIndex + offset];
+ if (!newSubView) return false;
+ this.selectedSubView = newSubView;
+ return true;
+ },
+
+ shouldBeFocusable_() {
+ return !this.tabsHidden && this.subViews_.length > 0;
+ },
+
+ updateFocusability_() {
+ if (this.shouldBeFocusable_()) {
+ Polymer.dom(this.$.tabs).setAttribute('tabindex', 0);
+ } else {
+ Polymer.dom(this.$.tabs).removeAttribute('tabindex');
+ }
+ },
+
+ computeRadioId_(subView) {
+ // We can't just use the tagName as the radio's ID because there are
+ // instances where a single subview type can handle multiple event types,
+ // and thus might be present multiple times in a single tab view. In order
+ // to avoid the case where we might have two tabs with the same ID, we
+ // uniquify this ID by appending the tab's label with all spaces replaced
+ // by dashes (because spaces aren't allowed in HTML IDs).
+ return subView.tagName + '-' + subView.tabLabel.replace(/ /g, '-');
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html
new file mode 100644
index 00000000000..d5e9c19e672
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/tab_view_test.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/power_series.html">
+<link rel="import" href="/tracing/ui/analysis/alert_sub_view.html">
+<link rel="import" href="/tracing/ui/analysis/multi_power_sample_sub_view.html">
+<link rel="import" href="/tracing/ui/base/tab_view.html">
+
+<dom-module id='tr-ui-b-tab-view-test-non-sub-view'>
+ <template>
+ <div></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+const nonSubViewBehavior = {};
+
+Polymer({
+ is: 'tr-ui-b-tab-view-test-non-sub-view',
+ behaviors: [nonSubViewBehavior]
+});
+
+tr.b.unittest.testSuite(function() {
+ function createPowerSampleSubView() {
+ const model = tr.c.TestUtils.newModel(function(m) {
+ m.device.powerSeries = new tr.model.PowerSeries(m.device);
+
+ m.device.vSyncTimestamps = [0];
+ m.device.powerSeries.addPowerSample(1, 1);
+ m.device.powerSeries.addPowerSample(2, 2);
+ m.device.powerSeries.addPowerSample(3, 3);
+ m.device.powerSeries.addPowerSample(4, 2);
+ });
+
+ const subView = document.createElement(
+ 'tr-ui-a-multi-power-sample-sub-view');
+ subView.selection = new tr.model.EventSet(model.device.powerSeries.samples);
+ subView.tabLabel = 'Power samples';
+ return subView;
+ }
+
+ function createAlertSubView() {
+ const slice = tr.c.TestUtils.newSliceEx(
+ {title: 'b', start: 0, duration: 0.002});
+ const alertInfo = new tr.model.EventInfo(
+ 'Alert 1', 'Critical alert',
+ [{
+ label: 'Example',
+ textContent: 'Example page',
+ href: 'http://www.example.com'
+ }]);
+
+ const alert = new tr.model.Alert(alertInfo, 5, [slice]);
+ const subView = document.createElement('tr-ui-a-alert-sub-view');
+ subView.selection = new tr.model.EventSet(alert);
+ subView.tabLabel = 'Alerts';
+ subView.tabIcon = { text: '\u26A0', style: 'color: red;' };
+
+ return subView;
+ }
+
+ test('instantiate_noTabs', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = 'No items selected.';
+ this.addHTMLOutput(tabView);
+ });
+
+ test('instantiate_oneTab', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = '1 item selected.';
+ tabView.addSubView(createPowerSampleSubView());
+ this.addHTMLOutput(tabView);
+ });
+
+ test('instantiate_twoTabs', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = '3 items selected.';
+ tabView.addSubView(createPowerSampleSubView());
+ tabView.addSubView(createAlertSubView());
+ this.addHTMLOutput(tabView);
+ });
+
+ test('clearSubViews_selectedSubViewNullAfter', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = '3 items selected.';
+ tabView.addSubView(createPowerSampleSubView());
+ tabView.addSubView(createAlertSubView());
+
+ tabView.clearSubViews();
+
+ assert.isUndefined(tabView.selectedSubView);
+ });
+
+ test('changeSelectedSubView', function() {
+ let selectedTabChangeEventCount = 0;
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.addEventListener('selected-tab-change', function() {
+ selectedTabChangeEventCount++;
+ });
+
+ assert.isUndefined(tabView.selectedSubView);
+ assert.strictEqual(selectedTabChangeEventCount, 0);
+
+ const view1 = createPowerSampleSubView();
+ tabView.addSubView(view1);
+ assert.strictEqual(tabView.selectedSubView, view1);
+ assert.strictEqual(selectedTabChangeEventCount, 1);
+
+ const view2 = createAlertSubView();
+ tabView.addSubView(view2);
+ assert.strictEqual(tabView.selectedSubView, view1);
+ assert.strictEqual(selectedTabChangeEventCount, 1);
+
+ tabView.selectedSubView = view2;
+ assert.strictEqual(tabView.selectedSubView, view2);
+ assert.strictEqual(selectedTabChangeEventCount, 2);
+ });
+
+ // Regression test: https://github.com/catapult-project/catapult/issues/2754
+ test('instantiate_twoTabsSwitch', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ tabView.label = '3 items selected.';
+ tabView.addSubView(createPowerSampleSubView());
+ tabView.addSubView(createAlertSubView());
+ this.addHTMLOutput(tabView);
+ Polymer.dom.flush();
+
+ tabView.selectedSubView = tabView.tabs[1];
+ Polymer.dom.flush();
+
+ const selectedLabel = tabView.$.tabs.querySelector(':checked ~ label');
+ assert.isTrue(selectedLabel && selectedLabel.innerText.includes('Alerts'));
+ });
+
+ // Regression test: https://github.com/catapult-project/catapult/issues/2755
+ test('instantiate_twoTabsSwitchAndChange', function() {
+ const tabView = document.createElement('tr-ui-b-tab-view');
+ this.addHTMLOutput(tabView);
+ tabView.addSubView(createPowerSampleSubView());
+ tabView.addSubView(createAlertSubView());
+ Polymer.dom.flush();
+
+ tabView.$.tabs.querySelectorAll('label')[2].click();
+ tabView.$.tabs.querySelectorAll('label')[1].click();
+ tabView.clearSubViews();
+ tabView.addSubView(createPowerSampleSubView());
+ Polymer.dom.flush();
+
+ assert.isTrue(!!tabView.$.tabs.querySelector(':checked'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table.html
new file mode 100644
index 00000000000..3d707fb4b87
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table.html
@@ -0,0 +1,1808 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<!--
+@fileoverview A container that constructs a table-like container.
+-->
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const TableFormat = {};
+
+ TableFormat.SelectionMode = {
+ // Selection disabled.
+ // Default highlight: none.
+ NONE: 0,
+
+ // Row selection mode.
+ // Default highlight: dark row.
+ ROW: 1,
+
+ // Cell selection mode.
+ // Default highlight: dark cell and light row.
+ CELL: 2
+ };
+
+ TableFormat.HighlightStyle = {
+ // Highlight depends on the current selection mode.
+ DEFAULT: 0,
+
+ // No highlight.
+ NONE: 1,
+
+ // Light highlight.
+ LIGHT: 2,
+
+ // Dark highlight.
+ DARK: 3
+ };
+
+ TableFormat.ColumnAlignment = {
+ LEFT: 0 /* default */,
+ RIGHT: 1
+ };
+
+ return {
+ TableFormat,
+ };
+});
+</script>
+
+<dom-module id="tr-ui-b-table">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+
+ table {
+ flex: 1 1 auto;
+ align-self: stretch;
+ border-collapse: separate;
+ border-spacing: 0;
+ border-width: 0;
+ -webkit-user-select: initial;
+ }
+
+ tr > td {
+ padding: 2px 4px 2px 4px;
+ vertical-align: top;
+ }
+
+ table > tbody:focus {
+ outline: none;
+ }
+ table > tbody:focus[selection-mode="row"] > tr[selected],
+ table > tbody:focus[selection-mode="cell"] > tr > td[selected],
+ table > tbody:focus > tr.empty-row > td {
+ outline: 1px dotted #666666;
+ outline-offset: -1px;
+ }
+
+ button.toggle-button {
+ height: 15px;
+ line-height: 60%;
+ vertical-align: middle;
+ width: 100%;
+ }
+
+ button > * {
+ height: 15px;
+ vertical-align: middle;
+ }
+
+ td.button-column {
+ width: 30px;
+ }
+
+ table > thead > tr > td.sensitive:hover {
+ background-color: #fcfcfc;
+ }
+
+ table > thead > tr > td {
+ font-weight: bold;
+ text-align: left;
+
+ background-color: #eee;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ border-top: 1px solid #ffffff;
+ border-bottom: 1px solid #aaa;
+ }
+
+ table > tfoot {
+ background-color: #eee;
+ font-weight: bold;
+ }
+
+ /* Light row and cell highlight. */
+ table > tbody[row-highlight-style="light"] > tr[selected],
+ table > tbody[cell-highlight-style="light"] > tr > td[selected] {
+ background-color: rgb(213, 236, 229); /* light turquoise */
+ }
+ table > tbody[row-highlight-style="light"] >
+ tr:not(.empty-row):not([selected]):hover,
+ table > tbody[cell-highlight-style="light"] >
+ tr:not(.empty-row):not([selected]) > td:hover {
+ background-color: #f6f6f6; /* light grey */
+ }
+
+ /* Dark row and cell highlight. */
+ table > tbody[row-highlight-style="dark"] > tr[selected],
+ table > tbody[cell-highlight-style="dark"] > tr > td[selected] {
+ background-color: rgb(103, 199, 165); /* turquoise */
+ }
+ table > tbody[row-highlight-style="dark"] >
+ tr:not(.empty-row):not([selected]):hover,
+ table > tbody[cell-highlight-style="dark"] >
+ tr:not(.empty-row):not([selected]) > td:hover {
+ background-color: #e6e6e6; /* grey */
+ }
+ table > tbody[row-highlight-style="dark"] > tr:hover[selected],
+ table > tbody[cell-highlight-style="dark"] > tr[selected] > td:hover {
+ background-color: rgb(171, 217, 202); /* semi-light turquoise */
+ }
+
+ table > colgroup > col[selected] {
+ background-color: #e6e6e6; /* grey */
+ }
+
+ table > tbody > tr.empty-row > td {
+ color: #666;
+ font-style: italic;
+ text-align: center;
+ }
+
+ table > tbody.has-footer > tr:last-child > td {
+ border-bottom: 1px solid #aaa;
+ }
+
+ table > tfoot > tr:first-child > td {
+ border-top: 1px solid #ffffff;
+ }
+
+ :host([zebra]) table tbody tr:nth-child(even) {
+ background-color: #f4f4f4;
+ }
+
+ expand-button {
+ -webkit-user-select: none;
+ cursor: pointer;
+ margin-right: 3px;
+ font-size: smaller;
+ height: 1rem;
+ }
+
+ expand-button.button-expanded {
+ transform: rotate(90deg);
+ }
+ </style>
+ <table>
+ <colgroup id="cols">
+ </colgroup>
+ <thead id="head">
+ </thead>
+ <tbody id="body">
+ </tbody>
+ <tfoot id="foot">
+ </tfoot>
+ </table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+(function() {
+ const RIGHT_ARROW = String.fromCharCode(0x25b6);
+ const UNSORTED_ARROW = String.fromCharCode(0x25BF);
+ const ASCENDING_ARROW = String.fromCharCode(0x25B4);
+ const DESCENDING_ARROW = String.fromCharCode(0x25BE);
+
+ const SelectionMode = tr.ui.b.TableFormat.SelectionMode;
+ const SelectionModeValues = new Set(Object.values(SelectionMode));
+ const HighlightStyle = tr.ui.b.TableFormat.HighlightStyle;
+ const HighlightStyleValues = new Set(Object.values(HighlightStyle));
+ const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment;
+ const ColumnAlignmentValues = new Set(Object.values(ColumnAlignment));
+
+ Polymer({
+ is: 'tr-ui-b-table',
+
+ created() {
+ this.selectionMode_ = SelectionMode.NONE;
+ this.rowHighlightStyle_ = HighlightStyle.DEFAULT;
+ this.cellHighlightStyle_ = HighlightStyle.DEFAULT;
+ this.selectedTableRowInfo_ = undefined;
+ this.selectedColumnIndex_ = undefined;
+
+ this.tableColumns_ = [];
+ this.tableRows_ = [];
+ this.tableRowsInfo_ = new WeakMap();
+ this.tableFooterRows_ = [];
+ this.tableFooterRowsInfo_ = new WeakMap();
+ this.sortColumnIndex_ = undefined;
+ this.sortDescending_ = false;
+ this.columnsWithExpandButtons_ = [];
+ this.headerCells_ = [];
+ this.showHeader_ = true;
+ this.emptyValue_ = undefined;
+ this.subRowsPropertyName_ = 'subRows';
+ this.customizeTableRowCallback_ = undefined;
+ this.defaultExpansionStateCallback_ = undefined;
+ this.userCanModifySortOrder_ = true;
+ this.computedFontSizePx_ = undefined;
+ },
+
+ ready() {
+ this.$.body.addEventListener(
+ 'keydown', this.onKeyDown_.bind(this), true);
+ this.$.body.addEventListener(
+ 'focus', this.onFocus_.bind(this), true);
+ },
+
+ clear() {
+ this.selectionMode_ = SelectionMode.NONE;
+ this.rowHighlightStyle_ = HighlightStyle.DEFAULT;
+ this.cellHighlightStyle_ = HighlightStyle.DEFAULT;
+ this.selectedTableRowInfo_ = undefined;
+ this.selectedColumnIndex_ = undefined;
+
+ Polymer.dom(this).textContent = '';
+ this.tableColumns_ = [];
+ this.tableRows_ = [];
+ this.tableRowsInfo_ = new WeakMap();
+ this.tableFooterRows_ = [];
+ this.tableFooterRowsInfo_ = new WeakMap();
+ this.sortColumnIndex_ = undefined;
+ this.sortDescending_ = false;
+ this.columnsWithExpandButtons_ = [];
+ this.headerCells_ = [];
+ this.showHeader_ = true;
+ this.emptyValue_ = undefined;
+ this.subRowsPropertyName_ = 'subRows';
+ this.defaultExpansionStateCallback_ = undefined;
+ this.userCanModifySortOrder_ = true;
+ },
+
+ set zebra(zebra) {
+ if (zebra) {
+ this.setAttribute('zebra', true);
+ } else {
+ this.removeAttribute('zebra');
+ }
+ },
+
+ get zebra() {
+ return this.getAttribute('zebra');
+ },
+
+ get showHeader() {
+ return this.showHeader_;
+ },
+
+ set showHeader(showHeader) {
+ this.showHeader_ = showHeader;
+ this.scheduleRebuildHeaders_();
+ },
+
+ set subRowsPropertyName(name) {
+ this.subRowsPropertyName_ = name;
+ },
+
+ /**
+ * This callback will be called whenever a body row is built
+ * for a userRow that has subRows and does not have an explicit
+ * isExpanded field.
+ * The callback should return true if the row should be expanded,
+ * or false if the row should be collapsed.
+ * @param {function(userRow, parentUserRow): boolean} cb The callback.
+ */
+ set defaultExpansionStateCallback(cb) {
+ this.defaultExpansionStateCallback_ = cb;
+ this.scheduleRebuildBody_();
+ },
+
+ /**
+ * This callback will be called whenever a body row is built.
+ * The callback's return value is ignored.
+ * @param {function(userRow, trElement)} cb The callback.
+ */
+ set customizeTableRowCallback(cb) {
+ this.customizeTableRowCallback_ = cb;
+ this.scheduleRebuildBody_();
+ },
+
+ get emptyValue() {
+ return this.emptyValue_;
+ },
+
+ set emptyValue(emptyValue) {
+ const previousEmptyValue = this.emptyValue_;
+ this.emptyValue_ = emptyValue;
+ if (this.tableRows_.length === 0 && emptyValue !== previousEmptyValue) {
+ this.scheduleRebuildBody_();
+ }
+ },
+
+ /**
+ * Data objects should have the following fields:
+ * mandatory: title, value
+ * optional: width {string}, cmp {function}, colSpan {number},
+ * showExpandButtons {boolean},
+ * align {tr.ui.b.TableFormat.ColumnAlignment}
+ *
+ * @param {Array} columns An array of data objects.
+ */
+ set tableColumns(columns) {
+ // Figure out the columns with expand buttons...
+ let columnsWithExpandButtons = [];
+ for (let i = 0; i < columns.length; i++) {
+ if (columns[i].showExpandButtons) {
+ columnsWithExpandButtons.push(i);
+ }
+ }
+ if (columnsWithExpandButtons.length === 0) {
+ // First column if none have specified.
+ columnsWithExpandButtons = [0];
+ }
+
+ // Sanity check columns.
+ for (let i = 0; i < columns.length; i++) {
+ const colInfo = columns[i];
+ if (colInfo.width === undefined) continue;
+
+ const hasExpandButton = columnsWithExpandButtons.includes(i);
+
+ const w = colInfo.width;
+ if (w) {
+ if (/\d+px/.test(w)) {
+ continue;
+ } else if (/\d+%/.test(w)) {
+ if (hasExpandButton) {
+ throw new Error('Columns cannot be %-sized and host ' +
+ ' an expand button');
+ }
+ } else {
+ throw new Error('Unrecognized width string');
+ }
+ }
+ }
+
+ // Try to preserve the user's sort choice.
+ // This is a 'best-effort' attempt, for example we compare columns by
+ // thier titles which can be HTML nodes in which case we might consider
+ // them different even if they look the same to the user.
+ let sortIndex = undefined;
+ const currentSortColumn = this.tableColumns[this.sortColumnIndex_];
+ if (currentSortColumn) {
+ for (const [i, column] of columns.entries()) {
+ if (currentSortColumn.title === column.title) {
+ sortIndex = i;
+ break;
+ }
+ }
+ }
+
+ // Commit the change.
+ this.tableColumns_ = columns;
+ this.headerCells_ = [];
+ this.columnsWithExpandButtons_ = columnsWithExpandButtons;
+ this.scheduleRebuildHeaders_();
+ this.sortColumnIndex = sortIndex;
+
+ // Blow away the table rows, too.
+ this.tableRows = this.tableRows_;
+ },
+
+ get tableColumns() {
+ return this.tableColumns_;
+ },
+
+ /**
+ * @param {Array} rows An array of 'row' objects with the following
+ * fields:
+ * optional: subRows An array of objects that have the same 'row'
+ * structure. Set subRowsPropertyName to use an
+ * alternative field name.
+ */
+ set tableRows(rows) {
+ this.selectedTableRowInfo_ = undefined;
+ this.selectedColumnIndex_ = undefined;
+ this.tableRows_ = rows;
+ this.tableRowsInfo_ = new WeakMap();
+ this.scheduleRebuildBody_();
+ },
+
+ get tableRows() {
+ return this.tableRows_;
+ },
+
+ set footerRows(rows) {
+ this.tableFooterRows_ = rows;
+ this.tableFooterRowsInfo_ = new WeakMap();
+ this.scheduleRebuildFooter_();
+ },
+
+ get footerRows() {
+ return this.tableFooterRows_;
+ },
+
+ get userCanModifySortOrder() {
+ return this.userCanModifySortOrder_;
+ },
+
+ set userCanModifySortOrder(userCanModifySortOrder) {
+ const newUserCanModifySortOrder = !!userCanModifySortOrder;
+ if (newUserCanModifySortOrder === this.userCanModifySortOrder_) {
+ return;
+ }
+
+ this.userCanModifySortOrder_ = newUserCanModifySortOrder;
+ this.scheduleRebuildHeaders_();
+ },
+
+ set sortColumnIndex(number) {
+ if (number === this.sortColumnIndex_) return;
+
+ if (number !== undefined) {
+ if (this.tableColumns_.length <= number) {
+ throw new Error('Column number ' + number + ' is out of bounds.');
+ }
+ if (!this.tableColumns_[number].cmp) {
+ throw new Error('Column ' + number + ' does not have a comparator.');
+ }
+ }
+
+ this.sortColumnIndex_ = number;
+ this.updateHeaderArrows_();
+ this.scheduleRebuildBody_();
+ this.dispatchSortingChangedEvent_();
+ },
+
+ get sortColumnIndex() {
+ return this.sortColumnIndex_;
+ },
+
+ set sortDescending(value) {
+ const newValue = !!value;
+
+ if (newValue !== this.sortDescending_) {
+ this.sortDescending_ = newValue;
+ this.updateHeaderArrows_();
+ this.scheduleRebuildBody_();
+ this.dispatchSortingChangedEvent_();
+ }
+ },
+
+ get sortDescending() {
+ return this.sortDescending_;
+ },
+
+ updateHeaderArrows_() {
+ for (let i = 0; i < this.headerCells_.length; i++) {
+ const headerCell = this.headerCells_[i];
+ const isColumnCurrentlySorted = i === this.sortColumnIndex_;
+ if (!this.tableColumns_[i].cmp ||
+ (!this.userCanModifySortOrder_ && !isColumnCurrentlySorted)) {
+ headerCell.sideContent = '';
+ continue;
+ }
+ if (!isColumnCurrentlySorted) {
+ headerCell.sideContent = UNSORTED_ARROW;
+ headerCell.sideContentDisabled = false;
+ continue;
+ }
+ headerCell.sideContent = this.sortDescending_ ?
+ DESCENDING_ARROW : ASCENDING_ARROW;
+ headerCell.sideContentDisabled = !this.userCanModifySortOrder_;
+ }
+ },
+
+ generateHeaderColumns_() {
+ const selectedTableColumnIndex = this.selectedTableColumnIndex;
+ Polymer.dom(this.$.cols).textContent = '';
+ for (let i = 0; i < this.tableColumns_.length; ++i) {
+ const colElement = document.createElement('col');
+ if (i === selectedTableColumnIndex) {
+ colElement.setAttribute('selected', true);
+ }
+ Polymer.dom(this.$.cols).appendChild(colElement);
+ }
+
+ this.headerCells_ = [];
+ Polymer.dom(this.$.head).textContent = '';
+ if (!this.showHeader_) return;
+
+ const tr = this.appendNewElement_(this.$.head, 'tr');
+ for (let i = 0; i < this.tableColumns_.length; i++) {
+ const td = this.appendNewElement_(tr, 'td');
+
+ const headerCell = document.createElement('tr-ui-b-table-header-cell');
+ headerCell.column = this.tableColumns_[i];
+
+ // If the table can be sorted by this column and the user can modify
+ // the sort order, attach a tap callback to the column.
+ if (this.tableColumns_[i].cmp) {
+ const isColumnCurrentlySorted = i === this.sortColumnIndex_;
+ if (isColumnCurrentlySorted) {
+ headerCell.sideContent = this.sortDescending_ ?
+ DESCENDING_ARROW : ASCENDING_ARROW;
+ if (!this.userCanModifySortOrder_) {
+ headerCell.sideContentDisabled = true;
+ }
+ }
+ if (this.userCanModifySortOrder_) {
+ Polymer.dom(td).classList.add('sensitive');
+ if (!isColumnCurrentlySorted) {
+ headerCell.sideContent = UNSORTED_ARROW;
+ }
+ headerCell.tapCallback = this.createSortCallback_(i);
+ }
+ }
+
+ Polymer.dom(td).appendChild(headerCell);
+ this.headerCells_.push(headerCell);
+ }
+ },
+
+ applySizes_() {
+ if (this.tableRows_.length === 0 && !this.showHeader) return;
+
+ let rowToRemoveSizing;
+ let rowToSize;
+ if (this.showHeader) {
+ rowToSize = Polymer.dom(this.$.head).children[0];
+ rowToRemoveSizing = Polymer.dom(this.$.body).children[0];
+ } else {
+ rowToSize = Polymer.dom(this.$.body).children[0];
+ rowToRemoveSizing = Polymer.dom(this.$.head).children[0];
+ }
+ for (let i = 0; i < this.tableColumns_.length; i++) {
+ if (rowToRemoveSizing && Polymer.dom(rowToRemoveSizing).children[i]) {
+ const tdToRemoveSizing = Polymer.dom(rowToRemoveSizing).children[i];
+ tdToRemoveSizing.style.minWidth = '';
+ tdToRemoveSizing.style.width = '';
+ }
+
+ // Apply sizing.
+ const td = Polymer.dom(rowToSize).children[i];
+
+ let delta;
+ if (this.columnsWithExpandButtons_.includes(i)) {
+ td.style.paddingLeft = this.basicIndentation_ + 'px';
+ delta = this.basicIndentation_ + 'px';
+ } else {
+ delta = undefined;
+ }
+
+ function calc(base, delta) {
+ if (delta) {
+ return 'calc(' + base + ' - ' + delta + ')';
+ }
+ return base;
+ }
+
+ const w = this.tableColumns_[i].width;
+ if (w) {
+ if (/\d+px/.test(w)) {
+ td.style.minWidth = calc(w, delta);
+ } else if (/\d+%/.test(w)) {
+ td.style.width = w;
+ } else {
+ throw new Error('Unrecognized width string: ' + w);
+ }
+ }
+ }
+ },
+
+ createSortCallback_(columnNumber) {
+ return function() {
+ if (!this.userCanModifySortOrder_) return;
+
+ const previousIndex = this.sortColumnIndex;
+ this.sortColumnIndex = columnNumber;
+ if (previousIndex !== columnNumber) {
+ this.sortDescending = false;
+ } else {
+ this.sortDescending = !this.sortDescending;
+ }
+ }.bind(this);
+ },
+
+ generateTableRowNodes_(tableSection, userRows, rowInfoMap,
+ indentation, lastAddedRow,
+ parentRowInfo) {
+ if (this.sortColumnIndex_ !== undefined &&
+ tableSection === this.$.body) {
+ userRows = userRows.slice(); // Don't mess with the input data.
+ userRows.sort(function(rowA, rowB) {
+ let c = this.tableColumns_[this.sortColumnIndex_].cmp(
+ rowA, rowB);
+ if (this.sortDescending_) {
+ c = -c;
+ }
+ return c;
+ }.bind(this));
+ }
+
+ for (let i = 0; i < userRows.length; i++) {
+ const userRow = userRows[i];
+ const rowInfo = this.getOrCreateRowInfoFor_(rowInfoMap, userRow,
+ parentRowInfo);
+ const htmlNode = this.getHTMLNodeForRowInfo_(
+ tableSection, rowInfo, rowInfoMap, indentation);
+
+ if (lastAddedRow === undefined) {
+ // Put first into the table.
+ Polymer.dom(tableSection).insertBefore(
+ htmlNode, Polymer.dom(tableSection).firstChild);
+ } else {
+ // This is shorthand for insertAfter(htmlNode, lastAdded).
+ const nextSiblingOfLastAdded = Polymer.dom(lastAddedRow).nextSibling;
+ Polymer.dom(tableSection).insertBefore(
+ htmlNode, nextSiblingOfLastAdded);
+ }
+
+ lastAddedRow = htmlNode;
+ if (!rowInfo.isExpanded) continue;
+
+ // Append subrows now.
+ lastAddedRow = this.generateTableRowNodes_(
+ tableSection, userRow[this.subRowsPropertyName_], rowInfoMap,
+ indentation + 1, lastAddedRow, rowInfo);
+ }
+ return lastAddedRow;
+ },
+
+ getOrCreateRowInfoFor_(rowInfoMap, userRow, parentRowInfo) {
+ let rowInfo = undefined;
+
+ if (rowInfoMap.has(userRow)) {
+ rowInfo = rowInfoMap.get(userRow);
+ } else {
+ rowInfo = {
+ userRow,
+ htmlNode: undefined,
+ parentRowInfo
+ };
+ rowInfoMap.set(userRow, rowInfo);
+ }
+
+ // Recompute isExpanded in case defaultExpansionStateCallback_ has
+ // changed.
+ rowInfo.isExpanded = this.getExpandedForUserRow_(userRow);
+
+ return rowInfo;
+ },
+
+ customizeTableRow_(userRow, trElement) {
+ if (!this.customizeTableRowCallback_) return;
+ this.customizeTableRowCallback_(userRow, trElement);
+ },
+
+ get basicIndentation_() {
+ if (this.computedFontSizePx_ === undefined) {
+ this.computedFontSizePx_ = parseInt(
+ getComputedStyle(this).fontSize) || 16;
+ }
+ return this.computedFontSizePx_ - 2;
+ },
+
+ getHTMLNodeForRowInfo_(tableSection, rowInfo,
+ rowInfoMap, indentation) {
+ if (rowInfo.htmlNode) {
+ this.customizeTableRow_(rowInfo.userRow, rowInfo.htmlNode);
+ return rowInfo.htmlNode;
+ }
+
+ const INDENT_SPACE = indentation * 16;
+ const INDENT_SPACE_NO_BUTTON = indentation * 16 + this.basicIndentation_;
+ const trElement = this.ownerDocument.createElement('tr');
+ rowInfo.htmlNode = trElement;
+ rowInfo.indentation = indentation;
+ trElement.rowInfo = rowInfo;
+ this.customizeTableRow_(rowInfo.userRow, trElement);
+
+ const isBodyRow = tableSection === this.$.body;
+ const isExpandableRow = rowInfo.userRow[this.subRowsPropertyName_] &&
+ rowInfo.userRow[this.subRowsPropertyName_].length;
+
+ for (let i = 0; i < this.tableColumns_.length;) {
+ const td = this.appendNewElement_(trElement, 'td');
+ td.columnIndex = i;
+
+ const column = this.tableColumns_[i];
+ const value = column.value(rowInfo.userRow);
+ const colSpan = column.colSpan ? column.colSpan : 1;
+ td.style.colSpan = colSpan;
+
+ switch (column.align) {
+ case undefined:
+ case ColumnAlignment.LEFT:
+ break;
+
+ case ColumnAlignment.RIGHT:
+ td.style.textAlign = 'right';
+ break;
+
+ default:
+ throw new Error('Invalid alignment of column at index=' + i +
+ ': ' + column.align);
+ }
+
+ if (this.doesColumnIndexSupportSelection(i)) {
+ Polymer.dom(td).classList.add('supports-selection');
+ }
+
+ if (this.columnsWithExpandButtons_.includes(i)) {
+ if (rowInfo.userRow[this.subRowsPropertyName_] &&
+ rowInfo.userRow[this.subRowsPropertyName_].length > 0) {
+ td.style.paddingLeft = INDENT_SPACE + 'px';
+ td.style.display = 'flex';
+ const expandButton = this.appendNewElement_(td, 'expand-button');
+ Polymer.dom(expandButton).textContent = RIGHT_ARROW;
+ if (rowInfo.isExpanded) {
+ Polymer.dom(expandButton).classList.add('button-expanded');
+ }
+ } else {
+ td.style.paddingLeft = INDENT_SPACE_NO_BUTTON + 'px';
+ }
+ }
+
+ if (value !== undefined) {
+ Polymer.dom(td).appendChild(
+ tr.ui.b.asHTMLOrTextNode(value, this.ownerDocument));
+ }
+
+ td.addEventListener('click', function(i, clickEvent) {
+ // Prevent automatically focusing on the table upon clicking on the
+ // table. Explicitly focus on it when appropriate (upon clicking on a
+ // selectable row/cell) instead.
+ clickEvent.preventDefault();
+
+ if (!isBodyRow && !isExpandableRow) return;
+
+ clickEvent.stopPropagation();
+
+ if (clickEvent.target.tagName === 'EXPAND-BUTTON') {
+ this.setExpandedForUserRow_(
+ tableSection, rowInfoMap,
+ rowInfo.userRow, !rowInfo.isExpanded);
+ return;
+ }
+
+ // If the row/cell can be selected and it's not selected yet,
+ // select it.
+ if (isBodyRow && this.selectionMode_ !== SelectionMode.NONE) {
+ let shouldSelect = false;
+ let shouldFocus = false;
+ switch (this.selectionMode_) {
+ case SelectionMode.ROW:
+ shouldSelect = this.selectedTableRowInfo_ !== rowInfo;
+ shouldFocus = true;
+ break;
+ case SelectionMode.CELL:
+ if (this.doesColumnIndexSupportSelection(i)) {
+ shouldSelect = this.selectedTableRowInfo_ !== rowInfo ||
+ this.selectedColumnIndex_ !== i;
+ shouldFocus = true;
+ }
+ break;
+ default:
+ throw new Error('Invalid selection mode ' +
+ this.selectionMode_);
+ }
+ if (shouldFocus) {
+ this.focus();
+ }
+ if (shouldSelect) {
+ this.didTableRowInfoGetClicked_(rowInfo, i);
+ return;
+ }
+ }
+
+ // Otherwise, if the row is expandable, expand/collapse it.
+ if (isExpandableRow) {
+ this.setExpandedForUserRow_(tableSection, rowInfoMap,
+ rowInfo.userRow, !rowInfo.isExpanded);
+ }
+ }.bind(this, i));
+
+ // Add a double-click handler for stepping into a row/cell (if
+ // applicable).
+ if (isBodyRow) {
+ td.addEventListener('dblclick', function(i, e) {
+ e.stopPropagation();
+ this.dispatchStepIntoEvent_(rowInfo, i);
+ }.bind(this, i));
+ }
+
+ i += colSpan;
+ }
+
+ return rowInfo.htmlNode;
+ },
+
+ removeSubNodes_(tableSection, rowInfo, rowInfoMap) {
+ if (rowInfo.userRow[this.subRowsPropertyName_] === undefined) return;
+
+ for (let i = 0;
+ i < rowInfo.userRow[this.subRowsPropertyName_].length; i++) {
+ const subRow = rowInfo.userRow[this.subRowsPropertyName_][i];
+ const subRowInfo = rowInfoMap.get(subRow);
+ if (!subRowInfo) continue;
+
+ const subNode = subRowInfo.htmlNode;
+ if (subNode && Polymer.dom(subNode).parentNode === tableSection) {
+ Polymer.dom(tableSection).removeChild(subNode);
+ this.removeSubNodes_(tableSection, subRowInfo, rowInfoMap);
+ }
+ }
+ },
+
+ scheduleRebuildHeaders_() {
+ this.headerDirty_ = true;
+ this.scheduleRebuild_();
+ },
+
+ scheduleRebuildBody_() {
+ this.bodyDirty_ = true;
+ this.scheduleRebuild_();
+ },
+
+ scheduleRebuildFooter_() {
+ this.footerDirty_ = true;
+ this.scheduleRebuild_();
+ },
+
+ scheduleRebuild_() {
+ if (this.rebuildPending_) return;
+
+ this.rebuildPending_ = true;
+ setTimeout(function() {
+ this.rebuildPending_ = false;
+ this.rebuild();
+ }.bind(this), 0);
+ },
+
+ rebuildIfNeeded_() {
+ this.rebuild();
+ },
+
+ rebuild() {
+ const wasBodyOrHeaderDirty = this.headerDirty_ || this.bodyDirty_;
+
+ if (this.headerDirty_) {
+ this.generateHeaderColumns_();
+ this.headerDirty_ = false;
+ }
+ if (this.bodyDirty_) {
+ Polymer.dom(this.$.body).textContent = '';
+ this.generateTableRowNodes_(
+ this.$.body,
+ this.tableRows_, this.tableRowsInfo_, 0,
+ undefined, undefined);
+ if (this.tableRows_.length === 0 && this.emptyValue_ !== undefined) {
+ const trElement = this.ownerDocument.createElement('tr');
+ Polymer.dom(this.$.body).appendChild(trElement);
+ Polymer.dom(trElement).classList.add('empty-row');
+ const td = this.ownerDocument.createElement('td');
+ Polymer.dom(trElement).appendChild(td);
+ td.colSpan = this.tableColumns_.length;
+ const emptyValue = this.emptyValue_;
+ Polymer.dom(td).appendChild(
+ tr.ui.b.asHTMLOrTextNode(emptyValue, this.ownerDocument));
+ }
+ this.bodyDirty_ = false;
+ }
+
+ if (wasBodyOrHeaderDirty) this.applySizes_();
+
+ if (this.footerDirty_) {
+ Polymer.dom(this.$.foot).textContent = '';
+ this.generateTableRowNodes_(
+ this.$.foot,
+ this.tableFooterRows_, this.tableFooterRowsInfo_, 0,
+ undefined, undefined);
+ if (this.tableFooterRowsInfo_.length) {
+ Polymer.dom(this.$.body).classList.add('has-footer');
+ } else {
+ Polymer.dom(this.$.body).classList.remove('has-footer');
+ }
+ this.footerDirty_ = false;
+ }
+ },
+
+ appendNewElement_(parent, tagName) {
+ const element = parent.ownerDocument.createElement(tagName);
+ Polymer.dom(parent).appendChild(element);
+ return element;
+ },
+
+ getExpandedForTableRow(userRow) {
+ this.rebuildIfNeeded_();
+ const rowInfo = this.tableRowsInfo_.get(userRow);
+ if (rowInfo === undefined) {
+ throw new Error('Row has not been seen, must expand its parents');
+ }
+ return rowInfo.isExpanded;
+ },
+
+ getExpandedForUserRow_(userRow) {
+ if (userRow[this.subRowsPropertyName_] === undefined) {
+ return false;
+ }
+ if (userRow[this.subRowsPropertyName_].length === 0) {
+ return false;
+ }
+ if (userRow.isExpanded) {
+ return true;
+ }
+ if ((userRow.isExpanded !== undefined) &&
+ (userRow.isExpanded === false)) {
+ return false;
+ }
+
+ const rowInfo = this.tableRowsInfo_.get(userRow);
+ if (rowInfo && rowInfo.isExpanded) {
+ return true;
+ }
+
+ if (this.defaultExpansionStateCallback_ === undefined) {
+ return false;
+ }
+
+ let parentUserRow = undefined;
+ if (rowInfo && rowInfo.parentRowInfo) {
+ parentUserRow = rowInfo.parentRowInfo.userRow;
+ }
+
+ return this.defaultExpansionStateCallback_(
+ userRow, parentUserRow);
+ },
+
+ setExpandedForTableRow(userRow, expanded) {
+ this.rebuildIfNeeded_();
+ const rowInfo = this.tableRowsInfo_.get(userRow);
+ if (rowInfo === undefined) {
+ throw new Error('Row has not been seen, must expand its parents');
+ }
+ return this.setExpandedForUserRow_(this.$.body, this.tableRowsInfo_,
+ userRow, expanded);
+ },
+
+ setExpandedForUserRow_(tableSection, rowInfoMap,
+ userRow, expanded) {
+ this.rebuildIfNeeded_();
+
+ const rowInfo = rowInfoMap.get(userRow);
+ if (rowInfo === undefined) {
+ throw new Error('Row has not been seen, must expand its parents');
+ }
+
+ const wasExpanded = rowInfo.isExpanded;
+
+ rowInfo.isExpanded = !!expanded;
+ // If no node, then nothing further needs doing.
+ if (rowInfo.htmlNode === undefined) return;
+
+ // If its detached, then nothing needs doing.
+ if (rowInfo.htmlNode.parentElement !== tableSection) {
+ return;
+ }
+
+ // Otherwise, rebuild.
+ const expandButton =
+ Polymer.dom(rowInfo.htmlNode).querySelector('expand-button');
+ if (rowInfo.isExpanded) {
+ Polymer.dom(expandButton).classList.add('button-expanded');
+ const lastAddedRow = rowInfo.htmlNode;
+ if (rowInfo.userRow[this.subRowsPropertyName_]) {
+ this.generateTableRowNodes_(
+ tableSection,
+ rowInfo.userRow[this.subRowsPropertyName_], rowInfoMap,
+ rowInfo.indentation + 1,
+ lastAddedRow, rowInfo);
+ }
+ } else {
+ Polymer.dom(expandButton).classList.remove('button-expanded');
+ this.removeSubNodes_(tableSection, rowInfo, rowInfoMap);
+ }
+
+ if (wasExpanded !== rowInfo.isExpanded) {
+ const e = new tr.b.Event('row-expanded-changed');
+ e.row = rowInfo.userRow;
+ this.dispatchEvent(e);
+ }
+
+ this.maybeUpdateSelectedRow_();
+ },
+
+ get selectionMode() {
+ return this.selectionMode_;
+ },
+
+ set selectionMode(selectionMode) {
+ if (!SelectionModeValues.has(selectionMode)) {
+ throw new Error('Invalid selection mode ' + selectionMode);
+ }
+ this.rebuildIfNeeded_();
+ this.selectionMode_ = selectionMode;
+ this.didSelectionStateChange_();
+ },
+
+ get rowHighlightStyle() {
+ return this.rowHighlightStyle_;
+ },
+
+ set rowHighlightStyle(rowHighlightStyle) {
+ if (!HighlightStyleValues.has(rowHighlightStyle)) {
+ throw new Error('Invalid row highlight style ' + rowHighlightStyle);
+ }
+ this.rebuildIfNeeded_();
+ this.rowHighlightStyle_ = rowHighlightStyle;
+ this.didSelectionStateChange_();
+ },
+
+ get resolvedRowHighlightStyle() {
+ if (this.rowHighlightStyle_ !== HighlightStyle.DEFAULT) {
+ return this.rowHighlightStyle_;
+ }
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ return HighlightStyle.NONE;
+ case SelectionMode.ROW:
+ return HighlightStyle.DARK;
+ case SelectionMode.CELL:
+ return HighlightStyle.LIGHT;
+ default:
+ throw new Error('Invalid selection mode ' + selectionMode);
+ }
+ },
+
+ get cellHighlightStyle() {
+ return this.cellHighlightStyle_;
+ },
+
+ set cellHighlightStyle(cellHighlightStyle) {
+ if (!HighlightStyleValues.has(cellHighlightStyle)) {
+ throw new Error('Invalid cell highlight style ' + cellHighlightStyle);
+ }
+ this.rebuildIfNeeded_();
+ this.cellHighlightStyle_ = cellHighlightStyle;
+ this.didSelectionStateChange_();
+ },
+
+ get resolvedCellHighlightStyle() {
+ if (this.cellHighlightStyle_ !== HighlightStyle.DEFAULT) {
+ return this.cellHighlightStyle_;
+ }
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ case SelectionMode.ROW:
+ return HighlightStyle.NONE;
+ case SelectionMode.CELL:
+ return HighlightStyle.DARK;
+ default:
+ throw new Error('Invalid selection mode ' + selectionMode);
+ }
+ },
+
+ setHighlightStyle_(highlightAttribute, resolvedHighlightStyle) {
+ switch (resolvedHighlightStyle) {
+ case HighlightStyle.NONE:
+ Polymer.dom(this.$.body).removeAttribute(highlightAttribute);
+ break;
+ case HighlightStyle.LIGHT:
+ Polymer.dom(this.$.body).setAttribute(highlightAttribute, 'light');
+ break;
+ case HighlightStyle.DARK:
+ Polymer.dom(this.$.body).setAttribute(highlightAttribute, 'dark');
+ break;
+ default:
+ throw new Error('Invalid resolved highlight style ' +
+ resolvedHighlightStyle);
+ }
+ },
+
+ didSelectionStateChange_() {
+ this.setHighlightStyle_('row-highlight-style',
+ this.resolvedRowHighlightStyle);
+ this.setHighlightStyle_('cell-highlight-style',
+ this.resolvedCellHighlightStyle);
+
+ this.removeSelectedState_();
+
+ switch (this.selectionMode_) {
+ case SelectionMode.ROW:
+ // TODO: Replace this.selectionMode_ with a proper Polymer attribute.
+ Polymer.dom(this.$.body).setAttribute('selection-mode', 'row');
+ Polymer.dom(this.$.body).setAttribute('tabindex', 0);
+ this.selectedColumnIndex_ = undefined;
+ break;
+ case SelectionMode.CELL:
+ Polymer.dom(this.$.body).setAttribute('selection-mode', 'cell');
+ Polymer.dom(this.$.body).setAttribute('tabindex', 0);
+ if (this.selectedTableRowInfo_ &&
+ this.selectedColumnIndex_ === undefined) {
+ const i = this.getFirstSelectableColumnIndex_();
+ if (i === -1) {
+ // No column is selectable.
+ this.selectedTableRowInfo_ = undefined;
+ } else {
+ this.selectedColumnIndex_ = i;
+ }
+ }
+ break;
+ case SelectionMode.NONE:
+ Polymer.dom(this.$.body).removeAttribute('selection-mode');
+ Polymer.dom(this.$.body).removeAttribute('tabindex');
+ this.$.body.blur(); // Remove focus (if applicable).
+ this.selectedTableRowInfo_ = undefined;
+ this.selectedColumnIndex_ = undefined;
+ break;
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+
+ this.maybeUpdateSelectedRow_();
+ },
+
+ maybeUpdateSelectedRow_() {
+ if (this.selectedTableRowInfo_ === undefined) return;
+
+ // selectedUserRow may not be visible
+ function isVisible(rowInfo) {
+ if (!rowInfo.htmlNode) return false;
+ return !!rowInfo.htmlNode.parentElement;
+ }
+ if (isVisible(this.selectedTableRowInfo_)) {
+ this.updateSelectedState_();
+ return;
+ }
+
+ this.removeSelectedState_();
+ let curRowInfo = this.selectedTableRowInfo_;
+ while (curRowInfo && !isVisible(curRowInfo)) {
+ curRowInfo = curRowInfo.parentRowInfo;
+ }
+
+ this.selectedTableRowInfo_ = curRowInfo;
+ if (this.selectedTableRowInfo_) {
+ this.updateSelectedState_();
+ } else {
+ this.selectedColumnIndex_ = undefined;
+ }
+ },
+
+ didTableRowInfoGetClicked_(rowInfo, columnIndex) {
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ return;
+
+ case SelectionMode.CELL:
+ if (!this.doesColumnIndexSupportSelection(columnIndex)) {
+ return;
+ }
+ if (this.selectedColumnIndex !== columnIndex) {
+ this.selectedColumnIndex = columnIndex;
+ }
+ // Fall through.
+
+ case SelectionMode.ROW:
+ if (this.selectedTableRowInfo_ !== rowInfo) {
+ this.selectedTableRow = rowInfo.userRow;
+ }
+ }
+ },
+
+ dispatchStepIntoEvent_(rowInfo, columnIndex) {
+ const e = new tr.b.Event('step-into');
+ e.tableRow = rowInfo.userRow;
+ e.tableColumn = this.tableColumns_[columnIndex];
+ e.columnIndex = columnIndex;
+ this.dispatchEvent(e);
+ },
+
+ /**
+ * If the selectionMode is CELL and a cell is selected,
+ * return an object containing the row, column, and value of the selected
+ * cell.
+ *
+ * @return {undefined|!Object}
+ */
+ get selectedCell() {
+ const row = this.selectedTableRow;
+ const columnIndex = this.selectedColumnIndex;
+ if (row === undefined || columnIndex === undefined ||
+ this.tableColumns_.length <= columnIndex) {
+ return undefined;
+ }
+ const column = this.tableColumns_[columnIndex];
+ return {
+ row,
+ column,
+ value: column.value(row)
+ };
+ },
+
+ /**
+ * If a column is selected, return the object describing the selected
+ * column.
+ *
+ * Columns can be selected independently of rows and cells. So it is
+ * possible to select column 0 and cell [0,0], or column 1 and cell [0,0],
+ * for example. See |selectedCell| for how to access the selected cell when
+ * the selectionMode is CELL.
+ *
+ * |selectedTableColumn| is entirely independent of |selectedColumnIndex|.
+ * When the table selectionMode is CELL, use |selectedTableRow| and
+ * |selectedColumnIndex| to find the selected cell.
+ * When one or more columns have |selectable:true|, then use
+ * |selectedTableColumn| to find the selected column, which may be either
+ * the same as or different from |selectedColumnIndex|, if a cell is also
+ * selected.
+ *
+ * @return {number|undefined}
+ */
+ get selectedTableColumnIndex() {
+ const cols = Polymer.dom(this.$.cols).children;
+ for (let i = 0; i < cols.length; ++i) {
+ if (cols[i].getAttribute('selected')) {
+ return i;
+ }
+ }
+ return undefined;
+ },
+
+ /**
+ * @param {number|undefined} index
+ */
+ set selectedTableColumnIndex(selectedIndex) {
+ const cols = Polymer.dom(this.$.cols).children;
+ for (let i = 0; i < cols.length; ++i) {
+ if (i === selectedIndex) {
+ cols[i].setAttribute('selected', true);
+ } else {
+ cols[i].removeAttribute('selected');
+ }
+ }
+ },
+
+ get selectedTableRow() {
+ if (!this.selectedTableRowInfo_) return undefined;
+ return this.selectedTableRowInfo_.userRow;
+ },
+
+ set selectedTableRow(userRow) {
+ this.rebuildIfNeeded_();
+ if (this.selectionMode_ === SelectionMode.NONE) {
+ throw new Error('Selection is off.');
+ }
+
+ let rowInfo;
+ if (userRow === undefined) {
+ rowInfo = undefined;
+ } else {
+ rowInfo = this.tableRowsInfo_.get(userRow);
+ if (!rowInfo) {
+ throw new Error('Row has not been seen, must expand its parents.');
+ }
+ }
+
+ const e = this.prepareToChangeSelection_();
+
+ if (!rowInfo) {
+ this.selectedColumnIndex_ = undefined;
+ } else {
+ switch (this.selectionMode_) {
+ case SelectionMode.ROW:
+ this.selectedColumnIndex_ = undefined;
+ break;
+
+ case SelectionMode.CELL:
+ if (this.selectedColumnIndex_ === undefined) {
+ const i = this.getFirstSelectableColumnIndex_();
+ if (i === -1) {
+ throw new Error('Cannot find a selectable column.');
+ }
+ this.selectedColumnIndex_ = i;
+ }
+ break;
+
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+ }
+
+ this.selectedTableRowInfo_ = rowInfo;
+ this.updateSelectedState_();
+ this.dispatchEvent(e);
+ },
+
+ prepareToChangeSelection_() {
+ const e = new tr.b.Event('selection-changed');
+ const previousSelectedRowInfo = this.selectedTableRowInfo_;
+ if (previousSelectedRowInfo) {
+ e.previousSelectedTableRow = previousSelectedRowInfo.userRow;
+ } else {
+ e.previousSelectedTableRow = undefined;
+ }
+
+ this.removeSelectedState_();
+
+ return e;
+ },
+
+ removeSelectedState_() {
+ this.setSelectedState_(false);
+ },
+
+ updateSelectedState_() {
+ this.setSelectedState_(true);
+ },
+
+ setSelectedState_(select) {
+ if (this.selectedTableRowInfo_ === undefined) return;
+
+ // Row selection.
+ const rowNode = this.selectedTableRowInfo_.htmlNode;
+ if (select) {
+ Polymer.dom(rowNode).setAttribute('selected', true);
+ } else {
+ Polymer.dom(rowNode).removeAttribute('selected');
+ }
+
+ // Cell selection (if applicable).
+ const cellNode = Polymer.dom(rowNode).children[this.selectedColumnIndex_];
+ if (!cellNode) return;
+ if (select) {
+ Polymer.dom(cellNode).setAttribute('selected', true);
+ } else {
+ Polymer.dom(cellNode).removeAttribute('selected');
+ }
+ },
+
+ doesColumnIndexSupportSelection(columnIndex) {
+ const columnInfo = this.tableColumns_[columnIndex];
+ const scs = columnInfo.supportsCellSelection;
+ if (scs === false) return false;
+ return true;
+ },
+
+ getFirstSelectableColumnIndex_() {
+ for (let i = 0; i < this.tableColumns_.length; i++) {
+ if (this.doesColumnIndexSupportSelection(i)) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ getSelectableNodeGivenTableRowNode_(htmlNode) {
+ switch (this.selectionMode_) {
+ case SelectionMode.ROW:
+ return htmlNode;
+
+ case SelectionMode.CELL:
+ return Polymer.dom(htmlNode).children[this.selectedColumnIndex_];
+
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+ },
+
+ get selectedColumnIndex() {
+ if (this.selectionMode_ !== SelectionMode.CELL) {
+ return undefined;
+ }
+ return this.selectedColumnIndex_;
+ },
+
+ set selectedColumnIndex(selectedColumnIndex) {
+ this.rebuildIfNeeded_();
+ if (this.selectionMode_ === SelectionMode.NONE) {
+ throw new Error('Selection is off.');
+ }
+ if (selectedColumnIndex < 0 ||
+ selectedColumnIndex >= this.tableColumns_.length) {
+ throw new Error('Invalid index');
+ }
+ if (!this.doesColumnIndexSupportSelection(selectedColumnIndex)) {
+ throw new Error('Selection is not supported on this column');
+ }
+
+ const e = this.prepareToChangeSelection_();
+ if (this.selectedColumnIndex_ === undefined) {
+ this.selectedTableRowInfo_ = undefined;
+ } else if (!this.selectedTableRowInfo_) {
+ if (this.tableRows_.length === 0) {
+ throw new Error('No available row to be selected');
+ }
+ this.selectedTableRowInfo_ =
+ this.tableRowsInfo_.get(this.tableRows_[0]);
+ }
+ this.selectedColumnIndex_ = selectedColumnIndex;
+ this.updateSelectedState_();
+ this.dispatchEvent(e);
+ },
+
+ onKeyDown_(e) {
+ if (this.selectionMode_ === SelectionMode.NONE) return;
+
+ const CODE_TO_COMMAND_NAMES = {
+ 13: 'ENTER',
+ 32: 'SPACE',
+ 37: 'ARROW_LEFT',
+ 38: 'ARROW_UP',
+ 39: 'ARROW_RIGHT',
+ 40: 'ARROW_DOWN'
+ };
+ const cmdName = CODE_TO_COMMAND_NAMES[e.keyCode];
+ if (cmdName === undefined) return;
+
+ e.stopPropagation();
+ e.preventDefault();
+ this.performKeyCommand_(cmdName);
+ },
+
+ onFocus_(e) {
+ // This method should be idempotent. If it can't be, then focus() must be
+ // updated.
+ if (this.selectionMode_ === SelectionMode.NONE ||
+ this.selectedTableRow ||
+ this.tableRows_.length === 0) {
+ return;
+ }
+
+ if (this.selectionMode_ === SelectionMode.CELL &&
+ this.getFirstSelectableColumnIndex_() === -1) {
+ // If there are no selectable columns in cell selection mode, don't do
+ // anything.
+ return;
+ }
+
+ this.selectedTableRow = this.tableRows_[0];
+ },
+
+ focus() {
+ this.$.body.focus();
+
+ // Need to manually call onFocus_ here: if the table is invisible for any
+ // reason, then the focus event will not fire, but the table may become
+ // visible later, and should reflect the focus accurately.
+ // If the table is already visible, then this will cause onFocus_ to be
+ // called multiple times. That shouldn't be a problem since onFocus_ is
+ // idempotent.
+ this.onFocus_();
+ },
+
+ blur() {
+ this.$.body.blur();
+ },
+
+ get isFocused() {
+ return this.root.activeElement === this.$.body;
+ },
+
+ performKeyCommand_(cmdName) {
+ this.rebuildIfNeeded_();
+
+ switch (cmdName) {
+ case 'ARROW_UP':
+ this.selectPreviousOrFirstRowIfPossible_();
+ return;
+
+ case 'ARROW_DOWN':
+ this.selectNextOrFirstRowIfPossible_();
+ return;
+
+ case 'ARROW_RIGHT':
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ return; // No action.
+ case SelectionMode.ROW:
+ this.expandRowAndSelectChildRowIfPossible_();
+ return;
+ case SelectionMode.CELL:
+ this.selectNextSelectableCellToTheRightIfPossible_();
+ return;
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+
+ case 'ARROW_LEFT':
+ switch (this.selectionMode_) {
+ case SelectionMode.NONE:
+ return; // No action.
+ case SelectionMode.ROW:
+ this.collapseRowOrSelectParentRowIfPossible_();
+ return;
+ case SelectionMode.CELL:
+ this.selectNextSelectableCellToTheLeftIfPossible_();
+ return;
+ default:
+ throw new Error('Invalid selection mode ' + this.selectionMode_);
+ }
+
+ case 'SPACE':
+ this.toggleRowExpansionStateIfPossible_();
+ return;
+
+ case 'ENTER':
+ this.stepIntoSelectionIfPossible_();
+ return;
+
+ default:
+ throw new Error('Unrecognized command ' + cmdName);
+ }
+ },
+
+ selectPreviousOrFirstRowIfPossible_() {
+ const prev = this.selectedTableRowInfo_ ?
+ this.selectedTableRowInfo_.htmlNode.previousElementSibling :
+ this.$.body.firstChild;
+ if (!prev) return;
+
+ if (this.selectionMode_ === SelectionMode.CELL &&
+ this.getFirstSelectableColumnIndex_() === -1) {
+ // If there are no selectable columns in cell selection mode, don't do
+ // anything.
+ return;
+ }
+ tr.ui.b.scrollIntoViewIfNeeded(prev);
+ this.selectedTableRow = prev.rowInfo.userRow;
+ },
+
+ selectNextOrFirstRowIfPossible_() {
+ this.getFirstSelectableColumnIndex_;
+ const next = this.selectedTableRowInfo_ ?
+ this.selectedTableRowInfo_.htmlNode.nextElementSibling :
+ this.$.body.firstChild;
+ if (!next) return;
+
+ if (this.selectionMode_ === SelectionMode.CELL &&
+ this.getFirstSelectableColumnIndex_() === -1) {
+ // If there are no selectable columns in cell selection mode, don't do
+ // anything.
+ return;
+ }
+ tr.ui.b.scrollIntoViewIfNeeded(next);
+ this.selectedTableRow = next.rowInfo.userRow;
+ },
+
+ expandRowAndSelectChildRowIfPossible_() {
+ const selectedRowInfo = this.selectedTableRowInfo_;
+ if (!selectedRowInfo ||
+ selectedRowInfo.userRow[this.subRowsPropertyName_] === undefined ||
+ selectedRowInfo.userRow[this.subRowsPropertyName_].length === 0) {
+ return;
+ }
+ if (!selectedRowInfo.isExpanded) {
+ this.setExpandedForTableRow(selectedRowInfo.userRow, true);
+ }
+ this.selectedTableRow =
+ selectedRowInfo.htmlNode.nextElementSibling.rowInfo.userRow;
+ },
+
+ collapseRowOrSelectParentRowIfPossible_() {
+ const selectedRowInfo = this.selectedTableRowInfo_;
+ if (!selectedRowInfo) return;
+
+ if (selectedRowInfo.isExpanded) {
+ // If the node is expanded, collapse it.
+ this.setExpandedForTableRow(selectedRowInfo.userRow, false);
+ } else {
+ // If the node is not expanded, select its parent.
+ const parentRowInfo = selectedRowInfo.parentRowInfo;
+ if (parentRowInfo) {
+ this.selectedTableRow = parentRowInfo.userRow;
+ }
+ }
+ },
+
+ selectNextSelectableCellToTheRightIfPossible_() {
+ if (!this.selectedTableRowInfo_ ||
+ this.selectedColumnIndex_ === undefined) {
+ return;
+ }
+ for (let i = this.selectedColumnIndex_ + 1; i < this.tableColumns_.length;
+ i++) {
+ if (this.doesColumnIndexSupportSelection(i)) {
+ this.selectedColumnIndex = i;
+ return;
+ }
+ }
+ },
+
+ selectNextSelectableCellToTheLeftIfPossible_() {
+ if (!this.selectedTableRowInfo_ ||
+ this.selectedColumnIndex_ === undefined) {
+ return;
+ }
+ for (let i = this.selectedColumnIndex_ - 1; i >= 0; i--) {
+ if (this.doesColumnIndexSupportSelection(i)) {
+ this.selectedColumnIndex = i;
+ return;
+ }
+ }
+ },
+
+ toggleRowExpansionStateIfPossible_() {
+ const selectedRowInfo = this.selectedTableRowInfo_;
+ if (!selectedRowInfo ||
+ selectedRowInfo.userRow[this.subRowsPropertyName_] === undefined ||
+ selectedRowInfo.userRow[this.subRowsPropertyName_].length === 0) {
+ return;
+ }
+ this.setExpandedForTableRow(selectedRowInfo.userRow,
+ !selectedRowInfo.isExpanded);
+ },
+
+ stepIntoSelectionIfPossible_() {
+ if (!this.selectedTableRowInfo_) return;
+ this.dispatchStepIntoEvent_(this.selectedTableRowInfo_,
+ this.selectedColumnIndex_);
+ },
+
+ dispatchSortingChangedEvent_() {
+ const e = new tr.b.Event('sort-column-changed');
+ e.sortColumnIndex = this.sortColumnIndex_;
+ e.sortDescending = this.sortDescending_;
+ this.dispatchEvent(e);
+ }
+ });
+})();
+</script>
+
+<dom-module id="tr-ui-b-table-header-cell">
+ <template>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ display: flex;
+ }
+
+ span {
+ flex: 0 1 auto;
+ }
+
+ #side {
+ -webkit-user-select: none;
+ flex: 0 0 auto;
+ padding-left: 2px;
+ padding-right: 2px;
+ vertical-align: top;
+ font-size: 15px;
+ font-family: sans-serif;
+ line-height: 85%;
+ margin-left: 5px;
+ }
+
+ #side.disabled {
+ color: rgb(140, 140, 140);
+ }
+
+ #title:empty, #side:empty {
+ display: none;
+ }
+ </style>
+
+ <span id="title"></span>
+ <span id="side"></span>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment;
+
+Polymer({
+ is: 'tr-ui-b-table-header-cell',
+
+ created() {
+ this.tapCallback_ = undefined;
+ this.cellTitle_ = '';
+ this.align_ = undefined;
+ this.selectable_ = false;
+ this.column_ = undefined;
+ },
+
+ ready() {
+ this.addEventListener('click', this.onTap_.bind(this));
+ },
+
+ set column(column) {
+ this.column_ = column;
+ this.align = column.align;
+ this.cellTitle = column.title;
+ },
+
+ get column() {
+ return this.column_;
+ },
+
+ set cellTitle(value) {
+ this.cellTitle_ = value;
+
+ const titleNode = tr.ui.b.asHTMLOrTextNode(
+ this.cellTitle_, this.ownerDocument);
+
+ this.$.title.innerText = '';
+
+ Polymer.dom(this.$.title).appendChild(titleNode);
+ },
+
+ get cellTitle() {
+ return this.cellTitle_;
+ },
+
+ set align(align) {
+ switch (align) {
+ case undefined:
+ case ColumnAlignment.LEFT:
+ this.style.justifyContent = '';
+ break;
+
+ case ColumnAlignment.RIGHT:
+ this.style.justifyContent = 'flex-end';
+ break;
+
+ default:
+ throw new Error('Invalid alignment of column (title=\'' +
+ this.cellTitle_ + '\'): ' + align);
+ }
+ this.align_ = align;
+ },
+
+ get align() {
+ return this.align_;
+ },
+
+ clearSideContent() {
+ Polymer.dom(this.$.side).textContent = '';
+ },
+
+ set sideContent(content) {
+ Polymer.dom(this.$.side).textContent = content;
+ this.$.side.style.display = content ? 'inline' : 'none';
+ },
+
+ get sideContent() {
+ return Polymer.dom(this.$.side).textContent;
+ },
+
+ set sideContentDisabled(sideContentDisabled) {
+ this.$.side.classList.toggle('disabled', sideContentDisabled);
+ },
+
+ get sideContentDisabled() {
+ return this.$.side.classList.contains('disabled');
+ },
+
+ set tapCallback(callback) {
+ this.style.cursor = 'pointer';
+ this.tapCallback_ = callback;
+ },
+
+ get tapCallback() {
+ return this.tapCallback_;
+ },
+
+ onTap_() {
+ if (this.tapCallback_) {
+ this.tapCallback_();
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html
new file mode 100644
index 00000000000..d7e8d427cb9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table_header_cell.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<dom-module id='tr-ui-b-table-header-cell'>
+ <template>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ display: flex;
+ }
+
+ span {
+ flex: 0 1 auto;
+ }
+
+ side-element {
+ -webkit-user-select: none;
+ flex: 1 0 auto;
+ padding-left: 4px;
+ vertical-align: top;
+ font-size: 15px;
+ font-family: sans-serif;
+ display: inline;
+ line-height: 85%;
+ }
+ </style>
+
+ <span id="title"></span><side-element id="side"></side-element>
+ </template>
+</dom-module>
+ <script>
+ 'use strict';
+
+ Polymer({
+ is: 'tr-ui-b-table-header-cell',
+
+ listeners: {
+ 'tap': 'onTap_'
+ },
+
+ created() {
+ this.tapCallback_ = undefined;
+ this.cellTitle_ = '';
+ },
+
+ set cellTitle(value) {
+ this.cellTitle_ = value;
+
+ const titleNode =
+ tr.ui.b.asHTMLOrTextNode(this.cellTitle_, this.ownerDocument);
+
+ this.$.title.innerText = '';
+ Polymer.dom(this.$.title).appendChild(titleNode);
+ },
+
+ get cellTitle() {
+ return this.cellTitle_;
+ },
+
+ clearSideContent() {
+ Polymer.dom(this.$.side).textContent = '';
+ },
+
+ set sideContent(content) {
+ Polymer.dom(this.$.side).textContent = content;
+ },
+
+ get sideContent() {
+ return Polymer.dom(this.$.side).textContent;
+ },
+
+ set tapCallback(callback) {
+ this.style.cursor = 'pointer';
+ this.tapCallback_ = callback;
+ },
+
+ get tapCallback() {
+ return this.tapCallback_;
+ },
+
+ onTap_() {
+ if (this.tapCallback_) {
+ this.tapCallback_();
+ }
+ }
+ });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html
new file mode 100644
index 00000000000..73e8aca4418
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/table_test.html
@@ -0,0 +1,2115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+ const SelectionMode = tr.ui.b.TableFormat.SelectionMode;
+ const HighlightStyle = tr.ui.b.TableFormat.HighlightStyle;
+ const ColumnAlignment = tr.ui.b.TableFormat.ColumnAlignment;
+
+ function isSelected(element) {
+ if (!element.hasAttribute('selected')) return false;
+ return element.getAttribute('selected') === 'true';
+ }
+
+ function simulateDoubleClick(element) {
+ // See https://developer.mozilla.org/en/docs/Web/API/MouseEvent#Example.
+ const event = new MouseEvent('dblclick', {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ });
+ return element.dispatchEvent(event);
+ }
+
+ test('rowExpandedChanged', function() {
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'Name',
+ value: row => row.value,
+ }
+ ];
+ table.tableRows = [{value: 'a', subRows: [{value: 'b'}]}];
+ let count = 0;
+ table.addEventListener('row-expanded-changed', e => ++count);
+ this.addHTMLOutput(table);
+ table.rebuild();
+
+ assert.strictEqual(0, count);
+
+ table.setExpandedForTableRow(table.tableRows[0], true);
+ assert.strictEqual(1, count);
+
+ table.setExpandedForTableRow(table.tableRows[0], true);
+ assert.strictEqual(1, count);
+
+ table.setExpandedForTableRow(table.tableRows[0], false);
+ assert.strictEqual(2, count);
+ });
+
+ test('instantiateEmptyTable_withoutEmptyValue', function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '300px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = [];
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ // Check that the width of the first column was set correctly (despite no
+ // body rows).
+ const firstColumnHeader = table.$.head.children[0].children[0];
+ assert.closeTo(firstColumnHeader.offsetWidth, 300, 20);
+
+ // Check that the first column has a non-empty header.
+ const firstColumnTitle = tr.ui.b.findDeepElementMatchingPredicate(
+ firstColumnHeader, function(element) {
+ return Polymer.dom(element).textContent === 'First Column';
+ });
+ assert.isDefined(firstColumnTitle);
+
+ // Check that empty value was not appended.
+ assert.lengthOf(table.$.body.children, 0);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('instantiateEmptyTable_withEmptyValue', function() {
+ // TODO(#4321): Switch to using skipped instead once it works
+ return; // https://github.com/catapult-project/catapult/issues/4320
+ /* eslint-disable no-unreachable */
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '300px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = [];
+ table.emptyValue = 'This table is left intentionally empty';
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ // Check that the width of the first column was set correctly (despite no
+ // body rows).
+ const firstColumnHeader = table.$.head.children[0].children[0];
+ assert.closeTo(firstColumnHeader.offsetWidth, 300, 20);
+
+ // Check that empty value was appended.
+ assert.lengthOf(table.$.body.children, 1);
+ /* eslint-enable no-unreachable */
+ });
+
+ test('instantiateNestedTableNoNests', function() {
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '200px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2'
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.emptyValue = 'THIS SHOULD NOT BE VISIBLE!!!';
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ // Check that empty value was not appended.
+ assert.lengthOf(table.$.body.children, 2);
+ });
+
+ test('sequentialRebuildsBehaveSanely', function() {
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '200px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2'
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+ const footerRows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2'
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.footerRows = footerRows;
+ table.rebuild();
+ table.rebuild();
+ assert.strictEqual(table.$.body.children.length, 2);
+ assert.strictEqual(table.$.foot.children.length, 2);
+
+ this.addHTMLOutput(table);
+ });
+
+ test('instantiateNestedTableWithNests', function() {
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '250px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; },
+ width: '50%'
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2',
+ subRows: [
+ {
+ firstData: 'Sub1 A1',
+ secondData: 'Sub1 A2'
+ },
+ {
+ firstData: 'Sub2 A1',
+ secondData: 'Sub2 A2',
+ subRows: [
+ {
+ firstData: 'SubSub1 A1',
+ secondData: 'SubSub1 A2'
+ },
+ {
+ firstData: 'SubSub2 A1',
+ secondData: 'SubSub2 A2'
+ }
+ ]
+ },
+ {
+ firstData: 'Sub3 A1',
+ secondData: 'Sub3 A2'
+ }
+ ]
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+ });
+
+ test('instantiateSortingCallbacksWithNests', function() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '50%'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; },
+ width: '250px',
+ cmp(rowA, rowB) {
+ return rowA.secondData.toString().localeCompare(
+ rowB.secondData.toString());
+ },
+ showExpandButtons: true
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2',
+ subRows: [
+ {
+ firstData: 'Sub1 A1',
+ secondData: 'Sub1 A2'
+ },
+ {
+ firstData: 'Sub2 A1',
+ secondData: 'Sub2 A2',
+ subRows: [
+ {
+ firstData: 'SubSub1 A1',
+ secondData: 'SubSub1 A2'
+ },
+ {
+ firstData: 'SubSub2 A1',
+ secondData: 'SubSub2 A2'
+ }
+ ]
+ },
+ {
+ firstData: 'Sub3 A1',
+ secondData: 'Sub3 A2'
+ }
+ ]
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const footerRows = [
+ {
+ firstData: 'F1',
+ secondData: 'F2',
+ subRows: [
+ {
+ firstData: 'Sub1F1',
+ secondData: 'Sub1F2'
+ },
+ {
+ firstData: 'Sub2F1',
+ secondData: 'Sub2F2',
+ subRows: [
+ {
+ firstData: 'SubSub1F1',
+ secondData: 'SubSub1F2'
+ },
+ {
+ firstData: 'SubSub2F1',
+ secondData: 'SubSub2F2'
+ }
+ ]
+ },
+ {
+ firstData: 'Sub3F1',
+ secondData: 'Sub3F2'
+ }
+ ]
+ },
+ {
+ firstData: 'F\'1',
+ secondData: 'F\'2'
+ }
+
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.footerRows = footerRows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ const button = THIS_DOC.createElement('button');
+ Polymer.dom(button).textContent = 'Sort By Col 0';
+ button.addEventListener('click', function() {
+ table.sortDescending = !table.sortDescending;
+ table.sortColumnIndex = 0;
+ });
+ table.rebuild();
+
+ this.addHTMLOutput(button);
+ });
+
+
+ test('instantiateNestedTableAlreadyExpanded', function() {
+ const columns = [
+ {
+ title: 'a',
+ value(row) { return row.a; },
+ width: '150px'
+ },
+ {
+ title: 'a',
+ value(row) { return row.b; },
+ width: '50%'
+ }
+ ];
+
+ const rows = [
+ {
+ a: 'aToplevel',
+ b: 'bToplevel',
+ isExpanded: true,
+ subRows: [
+ {
+ a: 'a1',
+ b: 'b1'
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ const a1El = tr.ui.b.findDeepElementMatchingPredicate(
+ table, e => Polymer.dom(e).textContent === 'a1');
+ assert.isDefined(a1El);
+
+ const bToplevelEl = tr.ui.b.findDeepElementMatchingPredicate(
+ table,
+ function(element) {
+ return Polymer.dom(element).textContent === 'bToplevel';
+ });
+ assert.isDefined(bToplevelEl);
+ const expandButton = Polymer.dom(bToplevelEl.parentElement)
+ .querySelector('expand-button');
+ assert.isTrue(Polymer.dom(expandButton).classList.contains(
+ 'button-expanded'));
+ });
+
+
+ test('subRowsThatAreRetrievedOnDemand', function() {
+ const columns = [
+ {
+ title: 'a',
+ value(row) { return row.a; },
+ width: '150px'
+ }
+ ];
+
+ const rows = [
+ {
+ a: 'row1',
+ subRows: [
+ {
+ b: 'row1.1',
+ get subRows() {
+ throw new Error('Shold not be called');
+ }
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+ });
+
+
+ test('instantiateTableWithHiddenHeader', function() {
+ const columns = [
+ {
+ title: 'a',
+ value(row) { return row.a; },
+ width: '150px'
+ },
+ {
+ title: 'a',
+ value(row) { return row.b; },
+ width: '50%'
+ }
+ ];
+
+ const rows = [
+ {
+ a: 'aToplevel',
+ b: 'bToplevel'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = false;
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ const tHead = table.$.head;
+ assert.strictEqual(table.$.head.children.length, 0);
+ assert.strictEqual(0, tHead.getBoundingClientRect().height);
+
+ table.showHeader = true;
+ table.rebuild();
+ table.showHeader = false;
+ table.rebuild();
+ assert.strictEqual(table.$.head.children.length, 0);
+ });
+
+
+ test('sortColumnsNotPossibleOnPercentSizedColumns', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px'
+ },
+ {
+ title: 'Value',
+ value(row) { return row.b; },
+ width: '100%',
+ showExpandButtons: true
+ }
+ ];
+
+ const table1 = document.createElement('tr-ui-b-table');
+ table1.showHeader = true;
+
+ assert.throws(function() {
+ table1.tableColumns = columns;
+ });
+ });
+
+ test('twoTablesFirstColumnMatching', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px'
+ },
+ {
+ title: 'Value',
+ value(row) { return row.b; },
+ width: '100%'
+ }
+ ];
+
+ const table1 = document.createElement('tr-ui-b-table');
+ table1.showHeader = true;
+ table1.tableColumns = columns;
+ table1.tableRows = [
+ {
+ a: 'first',
+ b: 'row'
+ }
+ ];
+ table1.rebuild();
+ this.addHTMLOutput(table1);
+
+ const table2 = document.createElement('tr-ui-b-table');
+ table2.showHeader = false;
+ table2.tableColumns = columns;
+ table2.tableRows = [
+ {
+ a: 'second',
+ b: 'row'
+ }
+ ];
+ table2.rebuild();
+ this.addHTMLOutput(table2);
+
+ const h1FirstCol = table1.$.head.children[0].children[0];
+ const h2FirstCol = table2.$.body.children[0].children[0];
+ assert.strictEqual(h1FirstCol.getBoundingClientRect().width,
+ h2FirstCol.getBoundingClientRect().width);
+ });
+
+ test('programmaticSorting', function() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.value; },
+ cmp(rowA, rowB) {
+ return rowA.value.toString().localeCompare(
+ rowB.value.toString());
+ }
+ }
+ ];
+
+ const rows = [
+ {
+ value: 'A1',
+ subRows: [
+ {
+ value: 'A1.1'
+ },
+ {
+ value: 'A1.2',
+ subRows: [
+ {
+ value: 'A1.2.1'
+ },
+ {
+ value: 'A1.2.2'
+ }
+ ]
+ },
+ {
+ value: 'A1.3'
+ }
+ ]
+ },
+ {
+ value: 'A2'
+ }
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ table.sortDescending = true;
+ table.sortColumnIndex = 0;
+ table.rebuild();
+ const r0 = table.$.body.children[0];
+ assert.strictEqual(r0.rowInfo.userRow, rows[1]);
+
+ const r1 = table.$.body.children[1];
+ assert.strictEqual(r1.rowInfo.userRow, rows[0]);
+ });
+
+ test('sortDispatchesEvent', function() {
+ const table = document.createElement('tr-ui-b-table');
+ const columns = [
+ {
+ title: 'Column 0',
+ value(row) { return row.value0; },
+ cmp(rowA, rowB) { return rowA.value0 - rowB.value0; }
+ },
+ {
+ title: 'Column 1',
+ value(row) { return row.value1; },
+ cmp(rowA, rowB) { return rowA.value1 - rowB.value1; }
+ }
+ ];
+
+ let sortColumnIndex = undefined;
+ let sortDescending = undefined;
+ let numListenerCalls = 0;
+ table.tableColumns = columns;
+ table.addEventListener('sort-column-changed', function(e) {
+ sortColumnIndex = e.sortColumnIndex;
+ sortDescending = e.sortDescending;
+ numListenerCalls++;
+ });
+ table.rebuild();
+
+ table.sortColumnIndex = 0;
+ assert.strictEqual(sortColumnIndex, 0);
+ assert.strictEqual(numListenerCalls, 1);
+
+ table.sortDescending = true;
+ assert.strictEqual(sortColumnIndex, 0);
+ assert.isTrue(sortDescending);
+ assert.strictEqual(numListenerCalls, 2);
+
+ table.sortColumnIndex = 1;
+ table.sortDescending = false;
+ assert.strictEqual(sortColumnIndex, 1);
+ assert.isFalse(sortDescending);
+ assert.strictEqual(numListenerCalls, 4);
+
+ table.sortColumnIndex = undefined;
+ assert.strictEqual(sortColumnIndex, undefined);
+ assert.strictEqual(numListenerCalls, 5);
+ });
+
+ test('sortingAfterExpand', function() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.value; },
+ cmp(rowA, rowB) {
+ return rowA.value.toString().localeCompare(
+ rowB.value.toString());
+ }
+ }
+ ];
+
+ const rows = [
+ {
+ value: 'A1',
+ isExpanded: true,
+ subRows: [
+ {
+ value: 'A1.1'
+ },
+ {
+ value: 'A1.2',
+ subRows: [
+ {
+ value: 'A1.2.1'
+ },
+ {
+ value: 'A1.2.2'
+ }
+ ]
+ },
+ {
+ value: 'A1.3'
+ }
+ ]
+ },
+ {
+ value: 'A2'
+ }
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ table.sortDescending = true;
+ table.sortColumnIndex = 0;
+ table.rebuild();
+ const r0 = table.$.body.children[0];
+ assert.strictEqual(r0.rowInfo.userRow, rows[1]);
+
+ const r1 = table.$.body.children[1];
+ assert.strictEqual(r1.rowInfo.userRow, rows[0]);
+
+ const r2 = table.$.body.children[2];
+ assert.strictEqual(r2.rowInfo.userRow, rows[0].subRows[2]);
+
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ });
+
+ function createSimpleOneColumnNestedTable() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.value; },
+ cmp(rowA, rowB) {
+ return rowA.value.toString().localeCompare(
+ rowB.value.toString());
+ }
+ }
+ ];
+
+ const rows = [
+ {
+ value: 'A1',
+ subRows: [
+ {
+ value: 'A1.1'
+ },
+ {
+ value: 'A1.2',
+ subRows: [
+ {
+ value: 'A1.2.1'
+ },
+ {
+ value: 'A1.2.2'
+ }
+ ]
+ },
+ {
+ value: 'A1.3'
+ }
+ ]
+ },
+ {
+ value: 'A2'
+ }
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ return table;
+ }
+
+ function createMultiColumnNestedTable() {
+ const table = document.createElement('tr-ui-b-table');
+
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.value; },
+ cmp(rowA, rowB) {
+ return rowA.value.toString().localeCompare(
+ rowB.value.toString());
+ },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'A',
+ value(row) { return row.a; },
+ width: '25%'
+ },
+ {
+ title: 'B',
+ value(row) { return row.b; },
+ width: '25%'
+ },
+ {
+ title: 'C',
+ value(row) { return row.c; },
+ width: '25%',
+ supportsCellSelection: false
+ },
+ {
+ title: 'D',
+ value(row) { return row.d; },
+ width: '25%'
+ }
+ ];
+
+ const rows = [
+ {
+ value: 'R1',
+ a: 1, b: 2, c: 3, d: 4,
+ subRows: [
+ {
+ value: 'R1.1',
+ a: 2, b: 3, c: 4, d: 1,
+ },
+ {
+ value: 'R1.2',
+ a: 3, b: 4, c: 1, d: 2,
+ }
+ ]
+ },
+ {
+ value: 'R2',
+ a: 3, b: 4, c: 1, d: 2
+ }
+ ];
+
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ return table;
+ }
+
+ test('expandAfterRebuild', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.rebuild();
+ const rows = table.tableRows;
+
+ this.addHTMLOutput(table);
+
+ table.rebuild();
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+ table.setExpandedForTableRow(rows[0], true);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ const r1 = table.$.body.children[1];
+ assert.strictEqual(r1.rowInfo.userRow, rows[0].subRows[0]);
+ });
+
+ test('tableSelection', function() {
+ const table = createMultiColumnNestedTable();
+ const rows = table.tableRows;
+
+ table.selectionMode = SelectionMode.ROW;
+ table.selectedTableRow = rows[0];
+ assert.isUndefined(table.selectedColumnIndex);
+
+ table.setExpandedForTableRow(rows[0], true);
+ table.selectedTableRow = rows[0].subRows[1];
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ table.selectionMode = SelectionMode.CELL;
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.setExpandedForTableRow(rows[0], false);
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.selectionMode = SelectionMode.NONE;
+ assert.strictEqual(table.selectedTableRow, undefined);
+
+ table.selectionMode = SelectionMode.ROW;
+ table.setExpandedForTableRow(rows[0].subRows[1], true);
+ this.addHTMLOutput(table);
+
+ assert.isTrue(table.$.body.hasAttribute('tabindex'));
+ });
+
+
+ test('keyMovement_rows', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.selectionMode = SelectionMode.ROW;
+ this.addHTMLOutput(table);
+
+ const rows = table.tableRows;
+ table.selectedTableRow = rows[0];
+
+ table.performKeyCommand_('ARROW_DOWN');
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+
+ table.performKeyCommand_('ARROW_UP');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+
+ // Enter on collapsed row should expand.
+ table.selectedTableRow = rows[0];
+ table.performKeyCommand_('SPACE');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('SPACE');
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ // Arrow right on collapsed row should expand.
+ table.selectedTableRow = rows[0];
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_DOWN');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[1]);
+
+ // Arrow left on collapsed item should select parent.
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+ // Arrow left on parent should collapse its children.
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ // Arrow right on expanded row should select first child.
+ table.selectedTableRow = rows[0];
+ table.setExpandedForTableRow(rows[0], true);
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+
+ // Arrow right on a non-expandable row should do nothing.
+ table.selectedTableRow = rows[1];
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ assert.isFalse(table.getExpandedForTableRow(rows[1]));
+ });
+
+ test('keyMovement_cells', function() {
+ const table = createMultiColumnNestedTable();
+ table.selectionMode = SelectionMode.CELL;
+ this.addHTMLOutput(table);
+
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ const rows = table.tableRows;
+ table.selectedTableRow = rows[1];
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ // No-op (leftmost selectable cell already selected).
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.performKeyCommand_('ARROW_UP');
+ // No-op (top row already selected).
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.performKeyCommand_('ARROW_UP');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 2);
+ // Right arrow should NOT expand nested rows in cell selection mode.
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 4);
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ // No-op (rightmost selectable cell already selected).
+ assert.strictEqual(table.selectedColumnIndex, 4);
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('SPACE');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 4);
+ // Space on collapsed row should expand it.
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_DOWN');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 4);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_LEFT');
+ // Left arrow should NOT move to parent row.
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 2);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[0]);
+ // No-op (leftmost selectable cell already selected).
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_UP');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ // No-op (leftmost selectable cell already selected).
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ // Left arrow should NOT collapse nested rows in cell selection mode.
+ assert.isTrue(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('SPACE');
+ assert.strictEqual(table.selectedTableRow, rows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ // Space on expanded row should collapse it.
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_DOWN');
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+
+ table.performKeyCommand_('ARROW_DOWN');
+ // No-op (bottom row already selected).
+ assert.strictEqual(table.selectedTableRow, rows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ assert.isFalse(table.getExpandedForTableRow(rows[0]));
+ });
+
+ test('focus_empty', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.tableRows = [];
+ table.emptyValue = 'This table is left intentionally empty';
+ this.addHTMLOutput(table);
+
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ assert.isFalse(table.isFocused);
+
+ for (const selectionMode of [SelectionMode.ROW, SelectionMode.CELL]) {
+ table.selectionMode = selectionMode;
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus.
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually unfocus.
+ table.blur();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus again.
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Unfocus via removing selection mode.
+ table.selectionMode = SelectionMode.NONE;
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+ }
+
+ // Re-enable selection mode (for interactive testing).
+ table.selectionMode = SelectionMode.ROW;
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+ });
+
+ test('focus_rows', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.selectionMode = SelectionMode.ROW;
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus.
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually unfocus.
+ table.blur();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Trigger focus via clicking.
+ table.$.body.children[1].children[0].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[1]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Unfocus via removing selection mode.
+ table.selectionMode = SelectionMode.NONE;
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Re-enable selection mode.
+ table.selectionMode = SelectionMode.ROW;
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Programatically select row (should NOT steal focus).
+ table.selectedTableRow = table.tableRows[0];
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Trigger focus on the already selected row by clicking.
+ table.$.body.children[0].children[0].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.isUndefined(table.selectedColumnIndex);
+ });
+
+ test('focus_cells', function() {
+ const table = createMultiColumnNestedTable();
+ table.selectionMode = SelectionMode.CELL;
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus.
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ // Manually unfocus.
+ table.blur();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ // Trigger focus via clicking.
+ table.$.body.children[1].children[4].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 4);
+
+ // Unfocus via removing selection mode.
+ table.selectionMode = SelectionMode.NONE;
+ assert.isFalse(table.$.body.hasAttribute('tabindex'));
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Re-enable selection mode.
+ table.selectionMode = SelectionMode.CELL;
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Clicking on an unselectable cell should NOT trigger focus.
+ table.$.body.children[1].children[0].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Programatically select cell (should NOT steal focus).
+ table.selectedTableRow = table.tableRows[0];
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+
+ // Trigger focus on the already selected cell by clicking.
+ table.$.body.children[0].children[1].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.strictEqual(table.selectedTableRow, table.tableRows[0]);
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ });
+
+ test('focus_allCellsUnselectable', function() {
+ const table = createMultiColumnNestedTable();
+ table.selectionMode = SelectionMode.CELL;
+ for (const c of table.tableColumns) {
+ c.supportsCellSelection = false;
+ }
+ table.tableColumns = table.tableColumns;
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isFalse(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Manually focus (no automatic selection).
+ table.focus();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+
+ // Trigger focus via clicking (no selection).
+ table.$.body.children[1].children[2].click();
+ assert.strictEqual(table.$.body.getAttribute('tabindex'), '0');
+ assert.isTrue(table.isFocused);
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+ });
+
+ test('RightArrowKeyWhenTableSorted', function() {
+ const table = createSimpleOneColumnNestedTable();
+ table.selectionMode = SelectionMode.ROW;
+ this.addHTMLOutput(table);
+ table.sortDescending = true;
+ table.sortColumnIndex = 0;
+ table.rebuild();
+ const rows = table.tableRows;
+
+ // Arrow right should select the first child showing up on the viewer,
+ // rather than first child in sub rows since sorted.
+ table.selectedTableRow = rows[0];
+ table.performKeyCommand_('ARROW_RIGHT');
+ assert.strictEqual(table.selectedTableRow, rows[0].subRows[2]);
+ });
+
+ test('reduceNumberOfColumnsAfterRebuild', function() {
+ // Create a table with two columns.
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '100px'
+ },
+ {
+ title: 'Second Column',
+ value(row) { return row.secondData; },
+ width: '100px'
+ }
+ ];
+
+ // Build the table.
+ table.rebuild();
+
+ // Check that reducing the number of columns doesn't throw an exception.
+ table.tableColumns = [
+ {
+ title: 'First Column',
+ value(row) { return row.firstData; },
+ width: '200px'
+ }
+ ];
+ });
+
+ test('rowHighlightDark', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Col1',
+ value(row) { return row.b; },
+ width: '33%'
+ },
+ {
+ title: 'Col2',
+ value(row) { return row.b * 2; },
+ width: '33%'
+ },
+ {
+ title: 'Col3',
+ value(row) { return row.b * 3; },
+ width: '33%'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = true;
+ table.rowHighlightStyle = HighlightStyle.DARK;
+ table.tableColumns = columns;
+ table.tableRows = [
+ {
+ a: 'first',
+ b: '1'
+ },
+ {
+ a: 'second',
+ b: '2'
+ }
+ ];
+ table.rebuild();
+ this.addHTMLOutput(table);
+ });
+
+ test('cellHighlightLight', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Col1',
+ value(row) { return row.b; },
+ width: '33%'
+ },
+ {
+ title: 'Col2',
+ value(row) { return row.b * 2; },
+ width: '33%'
+ },
+ {
+ title: 'Col3',
+ value(row) { return row.b * 3; },
+ width: '33%'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = true;
+ table.cellHighlightStyle = HighlightStyle.LIGHT;
+ table.tableColumns = columns;
+ table.tableRows = [
+ {
+ a: 'first',
+ b: '1'
+ },
+ {
+ a: 'second',
+ b: '2'
+ }
+ ];
+ table.rebuild();
+ this.addHTMLOutput(table);
+ });
+
+ test('cellSelectionBasic', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Col1',
+ value(row) { return row.b; },
+ width: '33%'
+ },
+ {
+ title: 'Col2',
+ value(row) { return row.b * 2; },
+ width: '33%'
+ },
+ {
+ title: 'Col3',
+ value(row) { return row.b * 3; },
+ width: '33%'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = true;
+ table.selectionMode = SelectionMode.CELL;
+ table.rowHighlightStyle = HighlightStyle.NONE;
+ table.tableColumns = columns;
+ table.tableRows = [
+ {
+ a: 'first',
+ b: '1'
+ },
+ {
+ a: 'second',
+ b: '2'
+ }
+ ];
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ table.selectedTableRow = table.tableRows[0];
+ assert.strictEqual(table.selectedColumnIndex, 1);
+ let selectedCell = table.selectedCell;
+ assert.strictEqual(selectedCell.row, table.tableRows[0]);
+ assert.strictEqual(selectedCell.column, columns[1]);
+ assert.strictEqual(selectedCell.value, '1');
+
+ table.performKeyCommand_('ARROW_DOWN');
+ table.performKeyCommand_('ARROW_RIGHT');
+ table.performKeyCommand_('ARROW_RIGHT');
+ table.performKeyCommand_('ARROW_LEFT');
+ assert.strictEqual(table.selectedTableRow, table.tableRows[1]);
+ assert.strictEqual(table.selectedColumnIndex, 2);
+ selectedCell = table.selectedCell;
+ assert.strictEqual(selectedCell.row, table.tableRows[1]);
+ assert.strictEqual(selectedCell.column, columns[2]);
+ assert.strictEqual(selectedCell.value, 4);
+
+ table.selectedTableRow = undefined;
+ assert.isUndefined(table.selectedTableRow);
+ assert.isUndefined(table.selectedColumnIndex);
+ assert.isUndefined(table.selectedColumnIndex);
+ assert.isUndefined(table.selectedCell);
+ });
+
+ test('cellSelectionNested', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Value',
+ value(row) { return row.b; },
+ width: '150px'
+ }
+ ];
+
+ const rows = [
+ {
+ a: 'parent',
+ b: '1',
+ subRows: [
+ {
+ a: 'child',
+ b: '2'
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.showHeader = true;
+ table.selectionMode = SelectionMode.CELL;
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ // Expand the parent row.
+ table.setExpandedForTableRow(rows[0], true);
+
+ // Select the second cell in the child row.
+ table.selectedTableRow = rows[0].subRows[0];
+ assert.isFalse(isSelected(table.$.body.children[0]));
+ assert.isFalse(isSelected(table.$.body.children[0].children[1]));
+ assert.isTrue(isSelected(table.$.body.children[1]));
+ assert.isTrue(isSelected(table.$.body.children[1].children[1]));
+
+ // Fold the parent row. The second cell in the parent row should be
+ // automatically selected.
+ table.setExpandedForTableRow(rows[0], false);
+ assert.isTrue(isSelected(table.$.body.children[0]));
+ assert.isTrue(isSelected(table.$.body.children[0].children[1]));
+
+ // Expand the parent row again. Only the second cell of the parent row
+ // should still be selected.
+ table.setExpandedForTableRow(rows[0], true);
+ assert.isTrue(isSelected(table.$.body.children[0]));
+ assert.isTrue(isSelected(table.$.body.children[0].children[1]));
+ assert.isFalse(isSelected(table.$.body.children[1]));
+ assert.isFalse(isSelected(table.$.body.children[1].children[1]));
+ });
+
+ test('resolvedHighlightStyle', function() {
+ const table = document.createElement('tr-ui-b-table');
+
+ // Undefined selection mode.
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.NONE);
+
+ // Row selection mode.
+ table.selectionMode = SelectionMode.ROW;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.DARK);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.NONE);
+
+ // Cell selection mode.
+ table.selectionMode = SelectionMode.CELL;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.LIGHT);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK);
+
+ // Explicit row highlight style.
+ table.rowHighlightStyle = HighlightStyle.NONE;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK);
+
+ // Explicit row and cell highlight styles.
+ table.cellHighlightStyle = HighlightStyle.LIGHT;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.NONE);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.LIGHT);
+
+ // Back to default highlight styles.
+ table.cellHighlightStyle = HighlightStyle.DEFAULT;
+ table.rowHighlightStyle = HighlightStyle.DEFAULT;
+ assert.strictEqual(table.resolvedRowHighlightStyle, HighlightStyle.LIGHT);
+ assert.strictEqual(table.resolvedCellHighlightStyle, HighlightStyle.DARK);
+ });
+
+ test('headersWithHtmlElements', function() {
+ const firstColumnTitle = document.createTextNode('First Column');
+ const secondColumnTitle = document.createElement('span');
+ secondColumnTitle.innerText = 'Second Column';
+ secondColumnTitle.style.color = 'blue';
+
+ const columns = [
+ {
+ title: firstColumnTitle,
+ value(row) { return row.firstData; },
+ width: '200px'
+ },
+ {
+ title: secondColumnTitle,
+ value(row) { return row.secondData; }
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: 'A1',
+ secondData: 'A2'
+ },
+ {
+ firstData: 'B1',
+ secondData: 'B2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ const firstColumnHeader = table.$.head.children[0].children[0].children[0];
+ const secondColumnHeader = table.$.head.children[0].children[1].children[0];
+ assert.strictEqual(Polymer.dom(firstColumnHeader.cellTitle).textContent,
+ 'First Column');
+ assert.strictEqual(Polymer.dom(secondColumnHeader.cellTitle).textContent,
+ 'Second Column');
+ });
+
+ test('align', function() {
+ const columns = [
+ {
+ title: 'a',
+ align: ColumnAlignment.RIGHT,
+ value(row) {
+ return row.a;
+ }
+ }
+ ];
+ const rows = [{a: 1}, {a: 'long-row-so-that-alignment-would-be-visible'}];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(
+ table.$.body.children[0].children[0].style.textAlign, 'right');
+ });
+
+ test('subRowsPropertyName', function() {
+ const columns = [
+ {
+ title: 'a',
+ value(row) {
+ return row.a;
+ }
+ }
+ ];
+ const rows = [
+ {
+ a: 1,
+ isExpanded: true,
+ children: [
+ {a: 2}
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.subRowsPropertyName = 'children';
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ assert.strictEqual(
+ '2', Polymer.dom(table.$.body.children[1].children[0]).textContent);
+ });
+
+ test('shouldNotRenderUndefined', function() {
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.firstData; }
+ }
+ ];
+
+ const rows = [
+ {
+ firstData: undefined,
+ secondData: 'A2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ // check that we don't have 'undefined' anywhere
+ assert.isTrue(Polymer.dom(table.$.body).innerHTML.indexOf('undefined') < 0);
+ });
+
+ test('customizeTableRowCallback', function() {
+ const columns = [
+ {
+ title: 'Column',
+ value(row) { return row.data; }
+ }
+ ];
+
+ const rows = [
+ {
+ data: 'data'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ let callbackCalled = false;
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.customizeTableRowCallback = function(userRow, trElement) {
+ callbackCalled = (userRow === rows[0]);
+ };
+ table.rebuild();
+ assert.isTrue(callbackCalled);
+
+ this.addHTMLOutput(table);
+
+ // The callback can also be set after the table is first built.
+ table.customizeTableRowCallback = function(userRow, trElement) {
+ callbackCalled = (userRow === rows[0]);
+ };
+
+ // Setting the customize callback should set the body dirty.
+ assert.isTrue(table.bodyDirty_);
+
+ callbackCalled = false;
+
+ // Don't bother waiting for the timeout.
+ table.rebuild();
+
+ assert.isTrue(callbackCalled);
+ });
+
+ test('selectionEdgeCases', function() {
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'Column',
+ value(row) { return row.data; },
+ supportsCellSelection: false
+ }
+ ];
+ table.tableRows = [{ data: 'body row' }];
+ table.footerRows = [{ data: 'footer row' }];
+ table.selectionMode = SelectionMode.ROW;
+ this.addHTMLOutput(table);
+
+ // Clicking on the body row should *not* throw an exception (despite the
+ // column not supporting cell selection).
+ table.$.body.children[0].children[0].click();
+
+ // Clicking on the footer row should *not* throw an exception (despite
+ // footer rows not being selectable in general).
+ table.$.foot.children[0].children[0].click();
+ });
+
+ test('defaultExpansionStateCallback', function() {
+ const columns = [
+ {
+ title: 'Name',
+ value(row) { return row.name; }
+ },
+ {
+ title: 'Value',
+ value(row) { return row.value; }
+ }
+ ];
+
+ const rows = [
+ {
+ name: 'A',
+ value: 10,
+ subRows: [
+ {
+ name: 'B',
+ value: 8,
+ subRows: [
+ {
+ name: 'C',
+ value: 4
+ },
+ {
+ name: 'D',
+ value: 4
+ }
+ ]
+ },
+ {
+ name: 'E',
+ value: 2,
+ subRows: [
+ {
+ name: 'F',
+ value: 1
+ },
+ {
+ name: 'G',
+ value: 1
+ }
+ ]
+ }
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ let cRow = tr.ui.b.findDeepElementMatchingPredicate(
+ table, function(element) {
+ return Polymer.dom(element).textContent === 'C';
+ });
+ assert.strictEqual(cRow, undefined);
+
+ let callbackCalled = false;
+ table.defaultExpansionStateCallback = function(row, parentRow) {
+ callbackCalled = true;
+
+ if (parentRow === undefined) return true;
+
+ if (row.value >= (parentRow.value * 0.8)) return true;
+
+ return false;
+ };
+
+ // Setting the callback should set the body dirty.
+ assert.isTrue(table.bodyDirty_);
+ assert.isFalse(callbackCalled);
+
+ table.rebuild();
+
+ assert.isTrue(callbackCalled);
+ cRow = tr.ui.b.findDeepElementMatchingPredicate(table, function(element) {
+ return Polymer.dom(element).textContent === 'C';
+ });
+ assert.isDefined(cRow);
+ });
+
+ test('sortExpanded', function() {
+ const columns = [
+ {
+ title: 'Name',
+ value(row) { return row.name; }
+ },
+ {
+ title: 'Value',
+ value(row) { return row.value; },
+ cmp(x, y) { return x.value - y.value; }
+ }
+ ];
+
+ const rows = [
+ {
+ name: 'A',
+ value: 10,
+ subRows: [
+ {
+ name: 'B',
+ value: 8
+ },
+ {
+ name: 'C',
+ value: 4
+ },
+ ]
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ function isB(row) {
+ return row.textContent === 'B';
+ }
+
+ // Check that 'A' row is not expanded.
+ assert.isUndefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB));
+
+ // Expand 'A' row.
+ table.setExpandedForTableRow(rows[0], true);
+
+ // Check that 'A' is expanded.
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB));
+
+ // Sort by value.
+ table.sortColumnIndex = 1;
+
+ // Rebuild the table synchronously instead of waiting for scheduleRebuild_'s
+ // setTimeout(0).
+ table.rebuild();
+
+ // Check that 'A' is still expanded.
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(table, isB));
+ });
+
+ test('shouldPreserveSortWhenColumnsChange', function() {
+ const name = {
+ title: 'Name',
+ value(row) { return row.name; },
+ };
+
+ const count = {
+ title: 'Count',
+ value(row) { return row.count; },
+ cmp: (rowA, rowB) => rowA.a - rowB.a,
+ };
+
+ const otherCount = {
+ title: 'Count',
+ value(row) { return row.count; },
+ cmp: (rowA, rowB) => rowA.a - rowB.a,
+ };
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [count];
+ table.sortColumnIndex = 0;
+ table.sortDescending = true;
+ table.rebuild();
+
+ this.addHTMLOutput(table);
+
+ table.tableColumns = [name, count];
+ table.rebuild();
+ assert.strictEqual(1, table.sortColumnIndex);
+ assert.isTrue(table.sortDescending);
+
+ table.sortDescending = false;
+ table.tableColumns = [otherCount, name];
+ table.rebuild();
+ assert.strictEqual(0, table.sortColumnIndex);
+ assert.isFalse(table.sortDescending);
+
+ table.tableColumns = [name];
+ table.rebuild();
+ assert.isUndefined(table.sortColumnIndex);
+ });
+
+ test('userCanModifySortOrder', function() {
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'Name',
+ value: row => row.name
+ },
+ {
+ title: 'colA',
+ value: row => row.a,
+ cmp: (rowA, rowB) => rowA.a - rowB.a
+ },
+ {
+ title: 'colB',
+ value: row => row.b,
+ cmp: (rowA, rowB) => rowA.b - rowB.b
+ }
+ ];
+ table.tableRows = [
+ {name: 'A', a: 42, b: 0},
+ {name: 'B', a: 89, b: 100},
+ {name: 'C', a: 65, b: -273.15}
+ ];
+ table.userCanModifySortOrder = false;
+ table.sortColumnIndex = 2;
+ table.sortDescending = true;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ const toggleButton = document.createElement('button');
+ Polymer.dom(toggleButton).textContent =
+ 'Toggle table.userCanModifySortOrder';
+ toggleButton.addEventListener('click', function() {
+ table.userCanModifySortOrder = !table.userCanModifySortOrder;
+ });
+ this.addHTMLOutput(toggleButton);
+
+ const unsetButton = document.createElement('button');
+ Polymer.dom(unsetButton).textContent = 'Unset sort order';
+ unsetButton.addEventListener('click', function() {
+ table.sortColumnIndex = undefined;
+ });
+ this.addHTMLOutput(unsetButton);
+ });
+
+ test('columnSelection', function() {
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = [
+ {
+ title: 'Name',
+ value: (row) => row.name
+ },
+ {
+ title: 'colA',
+ selectable: true,
+ value: (row) => row.a,
+ cmp: (rowA, rowB) => rowA.a - rowB.a
+ },
+ {
+ title: 'colB',
+ selectable: true,
+ value: (row) => row.b,
+ cmp: (rowA, rowB) => rowA.b - rowB.b
+ }
+ ];
+ table.tableRows = [
+ {name: 'foo', a: 42, b: -42},
+ {name: 'bar', a: 57, b: 133}
+ ];
+ table.rebuild();
+ table.selectionMode = SelectionMode.CELL;
+ this.addHTMLOutput(table);
+
+ table.selectedTableColumnIndex = 1;
+ let cols = tr.ui.b.findDeepElementMatchingPredicate(table,
+ e => e.tagName === 'COLGROUP').children;
+ assert.isNull(cols[0].getAttribute('selected'));
+ assert.strictEqual(cols[1].getAttribute('selected'), 'true');
+ assert.isNull(cols[2].getAttribute('selected'));
+ assert.strictEqual(1, table.selectedTableColumnIndex);
+
+ table.selectedTableColumnIndex = undefined;
+ cols = tr.ui.b.findDeepElementMatchingPredicate(table,
+ e => e.tagName === 'COLGROUP').children;
+ assert.isNull(cols[0].getAttribute('selected'));
+ assert.isNull(cols[1].getAttribute('selected'));
+ assert.isNull(cols[2].getAttribute('selected'));
+ assert.isUndefined(table.selectedTableColumnIndex);
+
+ table.selectedTableColumnIndex = 2;
+ cols = tr.ui.b.findDeepElementMatchingPredicate(table,
+ e => e.tagName === 'COLGROUP').children;
+ assert.isNull(cols[0].getAttribute('selected'));
+ assert.isNull(cols[1].getAttribute('selected'));
+ assert.strictEqual(cols[2].getAttribute('selected'), 'true');
+ assert.strictEqual(2, table.selectedTableColumnIndex);
+ });
+
+ test('stepInto', function() {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) { return row.a; },
+ width: '150px',
+ supportsCellSelection: false
+ },
+ {
+ title: 'Col1',
+ value(row) { return row.b; },
+ width: '33%'
+ },
+ {
+ title: 'Col2',
+ value(row) { return row.b * 2; },
+ width: '33%'
+ },
+ {
+ title: 'Col3',
+ value(row) { return row.b * 3; },
+ width: '33%'
+ }
+ ];
+ const rows = [
+ {
+ a: 'first',
+ b: '1'
+ },
+ {
+ a: 'second',
+ b: '2'
+ }
+ ];
+
+ const table = document.createElement('tr-ui-b-table');
+
+ const firedStepIntoEvents = [];
+ table.addEventListener('step-into', e => firedStepIntoEvents.push(e));
+
+ table.cellHighlightStyle = HighlightStyle.DARK;
+ table.tableColumns = columns;
+ table.tableRows = rows;
+ table.rebuild();
+ this.addHTMLOutput(table);
+
+ assert.lengthOf(firedStepIntoEvents, 0);
+
+ // Double click.
+ simulateDoubleClick(table.$.body.children[0].children[1]);
+ assert.lengthOf(firedStepIntoEvents, 1);
+ assert.strictEqual(firedStepIntoEvents[0].tableRow, rows[0]);
+ assert.strictEqual(firedStepIntoEvents[0].tableColumn, columns[1]);
+ assert.strictEqual(firedStepIntoEvents[0].columnIndex, 1);
+
+ simulateDoubleClick(table.$.body.children[1].children[3]);
+ assert.lengthOf(firedStepIntoEvents, 2);
+ assert.strictEqual(firedStepIntoEvents[1].tableRow, rows[1]);
+ assert.strictEqual(firedStepIntoEvents[1].tableColumn, columns[3]);
+ assert.strictEqual(firedStepIntoEvents[1].columnIndex, 3);
+
+ // Shift+Enter in cell selection mode.
+ table.selectionMode = SelectionMode.CELL;
+ table.selectedTableRow = rows[0];
+ table.selectedColumnIndex = 2;
+ table.performKeyCommand_('ENTER');
+ assert.lengthOf(firedStepIntoEvents, 3);
+ assert.strictEqual(firedStepIntoEvents[2].tableRow, rows[0]);
+ assert.strictEqual(firedStepIntoEvents[2].tableColumn, columns[2]);
+ assert.strictEqual(firedStepIntoEvents[2].columnIndex, 2);
+
+ // Shift+Enter in row selection mode.
+ table.selectionMode = SelectionMode.ROW;
+ table.selectedTableRow = rows[1];
+ table.performKeyCommand_('ENTER');
+ assert.lengthOf(firedStepIntoEvents, 4);
+ assert.strictEqual(firedStepIntoEvents[3].tableRow, rows[1]);
+ assert.isUndefined(firedStepIntoEvents[3].tableColumn);
+ assert.isUndefined(firedStepIntoEvents[3].columnIndex);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html
new file mode 100644
index 00000000000..e142afd50f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool.html
@@ -0,0 +1,327 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/slice.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the TimingTool class.
+ */
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Tool for taking time measurements in the TimelineTrackView using
+ * Viewportmarkers.
+ * @constructor
+ */
+ function TimingTool(viewport, targetElement) {
+ this.viewport_ = viewport;
+
+ // Prepare the event handlers to be added and removed repeatedly.
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.onDblClick_ = this.onDblClick_.bind(this);
+ this.targetElement_ = targetElement;
+
+ // Valid only during mousedown.
+ this.isMovingLeftEdge_ = false;
+ }
+
+ TimingTool.prototype = {
+
+ onEnterTiming(e) {
+ this.targetElement_.addEventListener('mousemove', this.onMouseMove_);
+ this.targetElement_.addEventListener('dblclick', this.onDblClick_);
+ },
+
+ onBeginTiming(e) {
+ if (!this.isTouchPointInsideTrackBounds_(e.clientX, e.clientY)) {
+ return;
+ }
+
+ const pt = this.getSnappedToEventPosition_(e);
+ this.mouseDownAt_(pt.x, pt.y);
+
+ this.updateSnapIndicators_(pt);
+ },
+
+ updateSnapIndicators_(pt) {
+ if (!pt.snapped) return;
+
+ const ir = this.viewport_.interestRange;
+ if (ir.min === pt.x) {
+ ir.leftSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height);
+ }
+ if (ir.max === pt.x) {
+ ir.rightSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height);
+ }
+ },
+
+ onUpdateTiming(e) {
+ const pt = this.getSnappedToEventPosition_(e);
+ this.mouseMoveAt_(pt.x, pt.y, true);
+ this.updateSnapIndicators_(pt);
+ },
+
+ onEndTiming(e) {
+ this.mouseUp_();
+ },
+
+ onExitTiming(e) {
+ this.targetElement_.removeEventListener('mousemove', this.onMouseMove_);
+ this.targetElement_.removeEventListener('dblclick', this.onDblClick_);
+ },
+
+ onMouseMove_(e) {
+ if (e.button) return;
+
+ const worldX = this.getWorldXFromEvent_(e);
+ this.mouseMoveAt_(worldX, e.clientY, false);
+ },
+
+ onDblClick_(e) {
+ // TODO(nduca): Implement dobuleclicking.
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+
+ isTouchPointInsideTrackBounds_(clientX, clientY) {
+ if (!this.viewport_ ||
+ !this.viewport_.modelTrackContainer ||
+ !this.viewport_.modelTrackContainer.canvas) {
+ return false;
+ }
+
+ const canvas = this.viewport_.modelTrackContainer.canvas;
+ const canvasRect = canvas.getBoundingClientRect();
+ if (clientX >= canvasRect.left && clientX <= canvasRect.right &&
+ clientY >= canvasRect.top && clientY <= canvasRect.bottom) {
+ return true;
+ }
+
+ return false;
+ },
+
+ mouseDownAt_(worldX, y) {
+ const ir = this.viewport_.interestRange;
+ const dt = this.viewport_.currentDisplayTransform;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio);
+
+ if (ir.isEmpty) {
+ ir.setMinAndMax(worldX, worldX);
+ ir.rightSelected = true;
+ this.isMovingLeftEdge_ = false;
+ return;
+ }
+
+
+ // Left edge test.
+ if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) {
+ ir.leftSelected = true;
+ ir.min = worldX;
+ this.isMovingLeftEdge_ = true;
+ return;
+ }
+
+ // Right edge test.
+ if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) {
+ ir.rightSelected = true;
+ ir.max = worldX;
+ this.isMovingLeftEdge_ = false;
+ return;
+ }
+
+ ir.setMinAndMax(worldX, worldX);
+ ir.rightSelected = true;
+ this.isMovingLeftEdge_ = false;
+ },
+
+ mouseMoveAt_(worldX, y, mouseDown) {
+ if (mouseDown) {
+ this.updateMovingEdge_(worldX);
+ return;
+ }
+
+ const ir = this.viewport_.interestRange;
+ const dt = this.viewport_.currentDisplayTransform;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio);
+
+ // Left edge test.
+ if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) {
+ ir.leftSelected = true;
+ ir.rightSelected = false;
+ return;
+ }
+
+ // Right edge test.
+ if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) {
+ ir.leftSelected = false;
+ ir.rightSelected = true;
+ return;
+ }
+
+ ir.leftSelected = false;
+ ir.rightSelected = false;
+ return;
+ },
+
+ updateMovingEdge_(newWorldX) {
+ const ir = this.viewport_.interestRange;
+ let a = ir.min;
+ let b = ir.max;
+ if (this.isMovingLeftEdge_) {
+ a = newWorldX;
+ } else {
+ b = newWorldX;
+ }
+
+ if (a <= b) {
+ ir.setMinAndMax(a, b);
+ } else {
+ ir.setMinAndMax(b, a);
+ }
+
+ if (ir.min === newWorldX) {
+ this.isMovingLeftEdge_ = true;
+ ir.leftSelected = true;
+ ir.rightSelected = false;
+ } else {
+ this.isMovingLeftEdge_ = false;
+ ir.leftSelected = false;
+ ir.rightSelected = true;
+ }
+ },
+
+ mouseUp_() {
+ const dt = this.viewport_.currentDisplayTransform;
+ const ir = this.viewport_.interestRange;
+
+ ir.leftSelected = false;
+ ir.rightSelected = false;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const minWidthValue = dt.xViewVectorToWorld(2 * pixelRatio);
+ if (ir.range < minWidthValue) {
+ ir.reset();
+ }
+ },
+
+ getWorldXFromEvent_(e) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const canvas = this.viewport_.modelTrackContainer.canvas;
+ const worldOffset = canvas.getBoundingClientRect().left;
+ const viewX = (e.clientX - worldOffset) * pixelRatio;
+ return this.viewport_.currentDisplayTransform.xViewToWorld(viewX);
+ },
+
+
+ /**
+ * Get the closest position of an event within a vertical range of the mouse
+ * position if possible, otherwise use the position of the mouse pointer.
+ * @param {MouseEvent} e Mouse event with the current mouse coordinates.
+ * @return {
+ * {Number} x, The x coordinate in world space.
+ * {Number} y, The y coordinate in world space.
+ * {Number} height, The height of the event.
+ * {boolean} snapped Whether the coordinates are from a snapped event or
+ * the mouse position.
+ * }
+ */
+ getSnappedToEventPosition_(e) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const EVENT_SNAP_RANGE = 16 * pixelRatio;
+
+ const modelTrackContainer = this.viewport_.modelTrackContainer;
+ const modelTrackContainerRect =
+ modelTrackContainer.getBoundingClientRect();
+
+ const viewport = this.viewport_;
+ const dt = viewport.currentDisplayTransform;
+ const worldMaxDist = dt.xViewVectorToWorld(EVENT_SNAP_RANGE);
+
+ const worldX = this.getWorldXFromEvent_(e);
+ const mouseY = e.clientY;
+
+ const selection = new tr.model.EventSet();
+
+ // Look at the track under mouse position first for better performance.
+ modelTrackContainer.addClosestEventToSelection(
+ worldX, worldMaxDist, mouseY, mouseY, selection);
+
+ // Look at all tracks visible on screen.
+ if (!selection.length) {
+ modelTrackContainer.addClosestEventToSelection(
+ worldX, worldMaxDist,
+ modelTrackContainerRect.top, modelTrackContainerRect.bottom,
+ selection);
+ }
+
+ let minDistX = worldMaxDist;
+ let minDistY = Infinity;
+ const pixWidth = dt.xViewVectorToWorld(1);
+
+ // Create result object with the mouse coordinates.
+ const result = {
+ x: worldX,
+ y: mouseY - modelTrackContainerRect.top,
+ height: 0,
+ snapped: false
+ };
+
+ const eventBounds = new tr.b.math.Range();
+ for (const event of selection) {
+ const track = viewport.trackForEvent(event);
+ const trackRect = track.getBoundingClientRect();
+
+ eventBounds.reset();
+ event.addBoundsToRange(eventBounds);
+ let eventX;
+ if (Math.abs(eventBounds.min - worldX) <
+ Math.abs(eventBounds.max - worldX)) {
+ eventX = eventBounds.min;
+ } else {
+ eventX = eventBounds.max;
+ }
+
+ const distX = eventX - worldX;
+
+ const eventY = trackRect.top;
+ const eventHeight = trackRect.height;
+ const distY = Math.abs(eventY + eventHeight / 2 - mouseY);
+
+ // Prefer events with a closer y position if their x difference is below
+ // the width of a pixel.
+ if ((distX <= minDistX || Math.abs(distX - minDistX) < pixWidth) &&
+ distY < minDistY) {
+ minDistX = distX;
+ minDistY = distY;
+
+ // Retrieve the event position from the hit.
+ result.x = eventX;
+ result.y = eventY +
+ modelTrackContainer.scrollTop - modelTrackContainerRect.top;
+ result.height = eventHeight;
+ result.snapped = true;
+ }
+ }
+
+ return result;
+ }
+ };
+
+ return {
+ TimingTool,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html
new file mode 100644
index 00000000000..c07319d3e97
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/timing_tool_test.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/timing_tool.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function create100PxWideViewportInto10WideWorld() {
+ const vp = new tr.ui.TimelineViewport(document.createElement('div'));
+ const tempDisplayTransform = new tr.ui.TimelineDisplayTransform();
+ tempDisplayTransform.xSetWorldBounds(0, 10, 100);
+ vp.setDisplayTransformImmediately(tempDisplayTransform);
+
+ assert.strictEqual(vp.currentDisplayTransform.xViewToWorld(0), 0);
+ assert.strictEqual(vp.currentDisplayTransform.xViewToWorld(100), 10);
+
+ return vp;
+ }
+
+ test('dragLeftInterestRegion', function() {
+ const vp = create100PxWideViewportInto10WideWorld();
+ vp.interestRange.min = 1;
+ vp.interestRange.max = 9;
+ const tool = new tr.ui.b.TimingTool(vp);
+
+ tool.mouseDownAt_(1.1, 0);
+ assert.isTrue(vp.interestRange.leftSelected);
+ tool.mouseMoveAt_(1.5, 0, true);
+ assert.strictEqual(vp.interestRange.min, 1.5);
+ tool.mouseUp_();
+ assert.strictEqual(vp.interestRange.min, 1.5);
+ assert.isFalse(vp.interestRange.leftSelected);
+ });
+
+ test('dragRightInterestRegion', function() {
+ const vp = create100PxWideViewportInto10WideWorld();
+ vp.interestRange.min = 1;
+ vp.interestRange.max = 9;
+ const tool = new tr.ui.b.TimingTool(vp);
+
+ tool.mouseDownAt_(9.1, 0);
+ assert.isTrue(vp.interestRange.rightSelected);
+ tool.mouseMoveAt_(8, 0, true);
+ assert.strictEqual(vp.interestRange.max, 8);
+ tool.mouseUp_();
+ assert.strictEqual(vp.interestRange.max, 8);
+ assert.isFalse(vp.interestRange.leftSelected);
+ });
+
+ test('dragInNewSpace', function() {
+ const vp = create100PxWideViewportInto10WideWorld();
+ vp.interestRange.min = 1;
+ vp.interestRange.max = 9;
+ const tool = new tr.ui.b.TimingTool(vp);
+
+ tool.mouseDownAt_(5, 0);
+ assert.isTrue(vp.interestRange.rightSelected);
+ assert.strictEqual(vp.interestRange.min, 5);
+ assert.strictEqual(vp.interestRange.max, 5);
+ tool.mouseMoveAt_(4, 0, true);
+ assert.strictEqual(vp.interestRange.min, 4);
+ assert.strictEqual(vp.interestRange.max, 5);
+ assert.isTrue(vp.interestRange.leftSelected);
+ tool.mouseUp_();
+ assert.strictEqual(vp.interestRange.min, 4);
+ assert.isFalse(vp.interestRange.leftSelected);
+ assert.isFalse(vp.interestRange.rightSelected);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html
new file mode 100644
index 00000000000..51108abd247
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+</script>
+<dom-module id='tr-ui-b-toolbar-button'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ background-color: #f8f8f8;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ color: rgba(0,0,0,0.8);
+ justify-content: center;
+ align-self: stretch;
+ min-width: 23px;
+ }
+
+ :host(:hover) {
+ background-color: rgba(255, 255, 255, 1.0);
+ border-color: rgba(0, 0, 0, 0.8);
+ box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 1);
+ }
+
+ #aligner {
+ display: flex;
+ flex: 0 0 auto;
+ align-self: center;
+ }
+ </style>
+ <div id="aligner">
+ <slot></slot>
+ </div>
+ </template>
+</dom-module>
+<script>
+ 'use strict';
+ Polymer({ is: 'tr-ui-b-toolbar-button' });
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html
new file mode 100644
index 00000000000..d111fb637d8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/toolbar_button_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/toolbar_button.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('tallWithTextContent', function() {
+ const el = document.createElement('tr-ui-b-toolbar-button');
+ el.style.width = '100px';
+ el.style.height = '40px';
+
+ Polymer.dom(el).textContent = 'blahblah';
+
+ this.addHTMLOutput(el);
+ });
+
+ test('tallWithInnerSpan', function() {
+ const el = document.createElement('tr-ui-b-toolbar-button');
+ el.style.width = '100px';
+ el.style.height = '40px';
+
+ Polymer.dom(el).appendChild(tr.ui.b.createSpan({textContent: 'blahblah'}));
+
+ this.addHTMLOutput(el);
+ });
+
+ test('puny', function() {
+ const el = document.createElement('tr-ui-b-toolbar-button');
+ Polymer.dom(el).textContent = 'M';
+ this.addHTMLOutput(el);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html
new file mode 100644
index 00000000000..a1e647e78b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui.html
@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ /**
+ * Decorates elements as an instance of a class.
+ * @param {string|!Element} source The way to find the element(s) to decorate.
+ * If this is a string then {@code querySeletorAll} is used to find the
+ * elements to decorate.
+ * @param {!Function} constr The constructor to decorate with. The constr
+ * needs to have a {@code decorate} function.
+ */
+ function decorate(source, constr) {
+ let elements;
+ if (typeof source === 'string') {
+ elements = Polymer.dom(tr.doc).querySelectorAll(source);
+ } else {
+ elements = [source];
+ }
+
+ for (let i = 0, el; el = elements[i]; i++) {
+ if (!(el instanceof constr)) {
+ constr.decorate(el);
+ }
+ }
+ }
+
+ /**
+ * Defines a tracing UI component, a function that can be called to construct
+ * the component.
+ *
+ * tr class:
+ * const List = tr.ui.b.define('list');
+ * List.prototype = {
+ * __proto__: HTMLUListElement.prototype,
+ * decorate: function() {
+ * ...
+ * },
+ * ...
+ * };
+ *
+ * Derived class:
+ * const CustomList = tr.ui.b.define('custom-list', List);
+ * CustomList.prototype = {
+ * __proto__: List.prototype,
+ * decorate: function() {
+ * ...
+ * },
+ * ...
+ * };
+ *
+ * @param {string} className The className of the newly created subtype. If
+ * subclassing by passing in opt_parentConstructor, this is used for
+ * debugging. If not subclassing, then it is the tag name that will be
+ * created by the component.
+
+ * @param {function=} opt_parentConstructor The parent class for this new
+ * element, if subclassing is desired. If provided, the parent class must
+ * be also a function created by tr.ui.b.define.
+ *
+ * @param {string=} opt_tagNS The namespace in which to create the base
+ * element. Has no meaning when opt_parentConstructor is passed and must
+ * either be undefined or the same namespace as the parent class.
+ *
+ * @return {function(Object=):Element} The newly created component
+ * constructor.
+ */
+ function define(className, opt_parentConstructor, opt_tagNS) {
+ if (typeof className === 'function') {
+ throw new Error('Passing functions as className is deprecated. Please ' +
+ 'use (className, opt_parentConstructor) to subclass');
+ }
+
+ className = className.toLowerCase();
+ if (opt_parentConstructor && !opt_parentConstructor.tagName) {
+ throw new Error('opt_parentConstructor was not ' +
+ 'created by tr.ui.b.define');
+ }
+
+ // Walk up the parent constructors until we can find the type of tag
+ // to create.
+ let tagName = className;
+ let tagNS = undefined;
+ if (opt_parentConstructor) {
+ if (opt_tagNS) {
+ throw new Error('Must not specify tagNS if parentConstructor is given');
+ }
+ let parent = opt_parentConstructor;
+ while (parent && parent.tagName) {
+ tagName = parent.tagName;
+ tagNS = parent.tagNS;
+ parent = parent.parentConstructor;
+ }
+ } else {
+ tagNS = opt_tagNS;
+ }
+
+ /**
+ * Creates a new UI element constructor.
+ * Arguments passed to the constuctor are provided to the decorate method.
+ * You will need to call the parent elements decorate method from within
+ * your decorate method and pass any required parameters.
+ * @constructor
+ */
+ function f() {
+ if (opt_parentConstructor &&
+ f.prototype.__proto__ !== opt_parentConstructor.prototype) {
+ throw new Error(
+ className + ' prototye\'s __proto__ field is messed up. ' +
+ 'It MUST be the prototype of ' + opt_parentConstructor.tagName);
+ }
+
+ let el;
+ if (tagNS === undefined) {
+ el = tr.doc.createElement(tagName);
+ } else {
+ el = tr.doc.createElementNS(tagNS, tagName);
+ }
+ f.decorate.call(this, el, arguments);
+ return el;
+ }
+
+ /**
+ * Decorates an element as a UI element class.
+ * @param {!Element} el The element to decorate.
+ */
+ f.decorate = function(el) {
+ el.__proto__ = f.prototype;
+ el.decorate.apply(el, arguments[1]);
+ el.constructor = f;
+ };
+
+ f.className = className;
+ f.tagName = tagName;
+ f.tagNS = tagNS;
+ f.parentConstructor = (opt_parentConstructor ? opt_parentConstructor :
+ undefined);
+ f.toString = function() {
+ if (!f.parentConstructor) {
+ return f.tagName;
+ }
+ return f.parentConstructor.toString() + '::' + f.className;
+ };
+
+ return f;
+ }
+
+ function elementIsChildOf(el, potentialParent) {
+ if (el === potentialParent) return false;
+
+ let cur = el;
+ while (Polymer.dom(cur).parentNode) {
+ if (cur === potentialParent) return true;
+ cur = Polymer.dom(cur).parentNode;
+ }
+ return false;
+ }
+
+ return {
+ decorate,
+ define,
+ elementIsChildOf,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html
new file mode 100644
index 00000000000..c87041eeb45
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/location.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const Location = tr.model.Location;
+
+ /**
+ * UIState is a class that represents the current state of the timeline by
+ * the Location of the point of interest and the current scaleX of the
+ * timeline.
+ *
+ * @constructor
+ */
+ function UIState(location, scaleX) {
+ this.location_ = location;
+ this.scaleX_ = scaleX;
+ }
+
+ /**
+ * Accepts a UIState string in the format of (timestamp)@(stableID)x(scaleX)
+ * Returns undefined if string is not in this format, or throws an Error if
+ * variables in a syntactically-correct stateString does not produce a valid
+ * UIState. Otherwise returns a constructed UIState instance.
+ */
+ UIState.fromUserFriendlyString = function(model, viewport, stateString) {
+ const navByFinderPattern = /^(-?\d+(\.\d+)?)@(.+)x(\d+(\.\d+)?)$/g;
+ const match = navByFinderPattern.exec(stateString);
+ if (!match) return;
+
+ const timestamp = parseFloat(match[1]);
+ const stableId = match[3];
+ const scaleX = parseFloat(match[4]);
+
+ if (scaleX <= 0) {
+ throw new Error('Invalid ScaleX value in UI State string.');
+ }
+
+ if (!viewport.containerToTrackMap.getTrackByStableId(stableId)) {
+ throw new Error('Invalid StableID given in UI State String.');
+ }
+
+ const loc = tr.model.Location.fromStableIdAndTimestamp(
+ viewport, stableId, timestamp);
+ return new UIState(loc, scaleX);
+ };
+
+ UIState.prototype = {
+
+ get location() {
+ return this.location_;
+ },
+
+ get scaleX() {
+ return this.scaleX_;
+ },
+
+ toUserFriendlyString(viewport) {
+ const timestamp = this.location_.xWorld;
+ const stableId =
+ this.location_.getContainingTrack(viewport).eventContainer.stableId;
+ const scaleX = this.scaleX_;
+ return timestamp.toFixed(5) + '@' + stableId + 'x' + scaleX.toFixed(5);
+ },
+
+ toDict() {
+ return {
+ location: this.location_.toDict(),
+ scaleX: this.scaleX_
+ };
+ }
+ };
+
+ return {
+ UIState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html
new file mode 100644
index 00000000000..de9c6ba6090
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_state_test.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/base/ui_state.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const UIState = tr.ui.b.UIState;
+
+ function FakeModel() {
+ this.processes = { 1: { threads: { 2: { stableId: '1.2' } } } };
+ }
+
+ // FakeTrack needs to be an instance of tr.ui.tracks.Track because a
+ // location is constructed in terms of Track instances.
+ function FakeTrack() { }
+ FakeTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ get eventContainer() {
+ return { stableId: '1.2' };
+ },
+
+ getBoundingClientRect() {
+ return { top: 5, height: 2 };
+ },
+
+ get parentElement() {
+ return null;
+ }
+ };
+
+ function FakeViewPort() {
+ this.containerToTrackMap = {
+ // "1.2" is the only valid stableId this test function accepts.
+ getTrackByStableId(stableId) {
+ if (stableId === '1.2') {
+ return new FakeTrack;
+ }
+ return undefined;
+ }
+ };
+ }
+
+ test('invalidStableId', function() {
+ const model = new FakeModel;
+ const vp = new FakeViewPort;
+ assert.throws(function() {
+ UIState.fromUserFriendlyString(model, vp, '15@1.3x6');
+ });
+ assert.throws(function() {
+ UIState.fromUserFriendlyString(model, vp, '15@2.2x6');
+ });
+ assert.throws(function() {
+ UIState.fromUserFriendlyString(model, vp, '505@1.x5');
+ });
+ });
+
+ test('invalidScaleX', function() {
+ const model = new FakeModel;
+ const vp = new FakeViewPort;
+ assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '1@1.2x-1'));
+ assert.throws(function() {
+ UIState.fromUserFriendlyString(model, vp, '1@1.2x0');
+ });
+ });
+
+ test('invalidSyntax', function() {
+ const model = new FakeModel;
+ const vp = new FakeViewPort;
+ assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '5'));
+ assert.isUndefined(UIState.fromUserFriendlyString(model, vp, '5@x5'));
+ assert.isUndefined(UIState.fromUserFriendlyString(model, vp, 'ab@1.2x5'));
+ });
+
+ test('validString', function() {
+ const model = new FakeModel;
+ const vp = new FakeViewPort;
+ const str = '-50125.51231@1.2x1.12345';
+ const uiState = UIState.fromUserFriendlyString(model, vp, str);
+
+ assert.isDefined(uiState);
+ assert.strictEqual(uiState.location.xWorld, -50125.51231);
+ assert.strictEqual(
+ uiState.location.getContainingTrack(vp).eventContainer.stableId,
+ '1.2');
+ assert.strictEqual(uiState.scaleX, 1.12345);
+
+ assert.strictEqual(uiState.toUserFriendlyString(vp), str);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html
new file mode 100644
index 00000000000..486c1019c62
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/ui_test.html
@@ -0,0 +1,246 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestElement = tr.ui.b.define('div');
+ TestElement.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ if (!this.decorateCallCount) {
+ this.decorateCallCount = 0;
+ }
+ this.decorateCallCount++;
+ }
+ };
+
+ const Base = tr.ui.b.define('div');
+ Base.prototype = {
+ __proto__: HTMLDivElement.prototype,
+ decorate() {
+ this.decoratedAsBase = true;
+ },
+ set baseProperty(v) {
+ this.basePropertySet = v;
+ }
+ };
+
+ test('decorateOnceViaNew', function() {
+ const testElement = new TestElement();
+ assert.strictEqual(testElement.decorateCallCount, 1);
+ });
+
+ test('decorateOnceDirectly', function() {
+ const testElement = document.createElement('div');
+ tr.ui.b.decorate(testElement, TestElement);
+ assert.strictEqual(testElement.decorateCallCount, 1);
+ });
+
+ test('componentToString', function() {
+ assert.strictEqual(Base.toString(), 'div');
+
+ const Sub = tr.ui.b.define('Sub', Base);
+ assert.strictEqual(Sub.toString(), 'div::sub');
+
+ const SubSub = tr.ui.b.define('Marine', Sub);
+ assert.strictEqual(SubSub.toString(), 'div::sub::marine');
+ });
+
+ test('basicDefines', function() {
+ const baseInstance = new Base();
+ assert.instanceOf(baseInstance, Base);
+ assert.isTrue(baseInstance.decoratedAsBase);
+
+ assert.strictEqual(baseInstance.constructor, Base);
+ assert.strictEqual(baseInstance.constructor.toString(), 'div');
+
+ baseInstance.basePropertySet = 7;
+ assert.strictEqual(baseInstance.basePropertySet, 7);
+ });
+
+ test('subclassing', function() {
+ const Sub = tr.ui.b.define('sub', Base);
+ Sub.prototype = {
+ __proto__: Base.prototype,
+ decorate() {
+ this.decoratedAsSub = true;
+ }
+ };
+
+ const subInstance = new Sub();
+ assert.instanceOf(subInstance, Sub);
+ assert.isTrue(subInstance.decoratedAsSub);
+
+ assert.instanceOf(subInstance, Base);
+ assert.isUndefined(subInstance.decoratedAsBase);
+
+ assert.strictEqual(subInstance.constructor, Sub);
+ assert.strictEqual(subInstance.constructor.toString(), 'div::sub');
+
+ subInstance.baseProperty = true;
+ assert.isTrue(subInstance.basePropertySet);
+ });
+
+ const NoArgs = tr.ui.b.define('div');
+ NoArgs.prototype = {
+ __proto__: HTMLDivElement.prototype,
+ decorate() {
+ this.noArgsDecorated_ = true;
+ },
+ get noArgsDecorated() {
+ return this.noArgsDecorated_;
+ }
+ };
+
+ const Args = tr.ui.b.define('args', NoArgs);
+ Args.prototype = {
+ __proto__: NoArgs.prototype,
+ decorate(first) {
+ this.first_ = first;
+ this.argsDecorated_ = true;
+ },
+ get first() {
+ return this.first_;
+ },
+ get argsDecorated() {
+ return this.argsDecorated_;
+ }
+ };
+
+ const ArgsChild = tr.ui.b.define('args-child', Args);
+ ArgsChild.prototype = {
+ __proto__: Args.prototype,
+ decorate(_, second) {
+ this.second_ = second;
+ this.argsChildDecorated_ = true;
+ },
+ get second() {
+ return this.second_;
+ },
+ get decorated() {
+ return this.decorated_;
+ },
+ get argsChildDecorated() {
+ return this.argsChildDecorated_ = true;
+ }
+ };
+
+ const ArgsDecoratingChild = tr.ui.b.define('args-decorating-child', Args);
+ ArgsDecoratingChild.prototype = {
+ __proto__: Args.prototype,
+ decorate(first, second) {
+ Args.prototype.decorate.call(this, first);
+ this.second_ = second;
+ this.argsDecoratingChildDecorated_ = true;
+ },
+ get second() {
+ return this.second_;
+ },
+ get decorated() {
+ return this.decorated_;
+ },
+ get argsDecoratingChildDecorated() {
+ return this.argsChildDecorated_ = true;
+ }
+ };
+
+ test('decorate_noArguments', function() {
+ let noArgs;
+ assert.doesNotThrow(function() {
+ noArgs = new NoArgs();
+ });
+ assert.isTrue(noArgs.noArgsDecorated);
+ });
+
+ test('decorate_arguments', function() {
+ const args = new Args('this is first');
+ assert.strictEqual(args.first, 'this is first');
+ assert.isTrue(args.argsDecorated);
+ assert.isUndefined(args.noArgsDecorated);
+ });
+
+ test('decorate_subclassArguments', function() {
+ const argsChild = new ArgsChild('this is first', 'and second');
+ assert.isUndefined(argsChild.first);
+ assert.strictEqual(argsChild.second, 'and second');
+
+ assert.isTrue(argsChild.argsChildDecorated);
+ assert.isUndefined(argsChild.argsDecorated);
+ assert.isUndefined(argsChild.noArgsDecorated);
+ });
+
+ test('decorate_subClassCallsParentDecorate', function() {
+ const argsDecoratingChild = new ArgsDecoratingChild(
+ 'this is first', 'and second');
+ assert.strictEqual(argsDecoratingChild.first, 'this is first');
+ assert.strictEqual(argsDecoratingChild.second, 'and second');
+ assert.isTrue(argsDecoratingChild.argsDecoratingChildDecorated);
+ assert.isTrue(argsDecoratingChild.argsDecorated);
+ assert.isUndefined(argsDecoratingChild.noArgsDecorated);
+ });
+
+ test('defineWithNamespace', function() {
+ const svgNS = 'http://www.w3.org/2000/svg';
+ const cls = tr.ui.b.define('svg', undefined, svgNS);
+ cls.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ Polymer.dom(this).setAttribute('width', 200);
+ Polymer.dom(this).setAttribute('height', 200);
+ Polymer.dom(this).setAttribute('viewPort', '0 0 200 200');
+ const rectEl = document.createElementNS(svgNS, 'rect');
+ Polymer.dom(rectEl).setAttribute('x', 10);
+ Polymer.dom(rectEl).setAttribute('y', 10);
+ Polymer.dom(rectEl).setAttribute('width', 180);
+ Polymer.dom(rectEl).setAttribute('height', 180);
+ Polymer.dom(this).appendChild(rectEl);
+ }
+ };
+ const el = new cls();
+ assert.strictEqual(el.tagName, 'svg');
+ assert.strictEqual(el.namespaceURI, svgNS);
+ this.addHTMLOutput(el);
+ });
+
+ test('defineSubclassWithNamespace', function() {
+ const svgNS = 'http://www.w3.org/2000/svg';
+ const cls = tr.ui.b.define('svg', undefined, svgNS);
+ cls.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ Polymer.dom(this).setAttribute('width', 200);
+ Polymer.dom(this).setAttribute('height', 200);
+ Polymer.dom(this).setAttribute('viewPort', '0 0 200 200');
+ const rectEl = document.createElementNS(svgNS, 'rect');
+ Polymer.dom(rectEl).setAttribute('x', 10);
+ Polymer.dom(rectEl).setAttribute('y', 10);
+ Polymer.dom(rectEl).setAttribute('width', 180);
+ Polymer.dom(rectEl).setAttribute('height', 180);
+ Polymer.dom(this).appendChild(rectEl);
+ }
+ };
+
+ const subCls = tr.ui.b.define('sub', cls);
+ subCls.prototype = {
+ __proto__: cls.prototype
+ };
+ assert.strictEqual(subCls.toString(), 'svg::sub');
+
+ const el = new subCls();
+ this.addHTMLOutput(el);
+ assert.strictEqual(el.tagName, 'svg');
+ assert.strictEqual(el.namespaceURI, svgNS);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html b/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html
new file mode 100644
index 00000000000..cca1003cc06
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/utils.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/math/rect.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ function instantiateTemplate(selector, doc) {
+ doc = doc || document;
+ const el = Polymer.dom(doc).querySelector(selector);
+ if (!el) {
+ throw new Error('Element not found: ' + selector);
+ }
+ return doc.importNode(el.content, true);
+ // return el.createInstance();
+ }
+
+ function windowRectForElement(element) {
+ const position = [element.offsetLeft, element.offsetTop];
+ const size = [element.offsetWidth, element.offsetHeight];
+ let node = element.offsetParent;
+ while (node) {
+ position[0] += node.offsetLeft;
+ position[1] += node.offsetTop;
+ node = node.offsetParent;
+ }
+ return tr.b.math.Rect.fromXYWH(position[0], position[1], size[0], size[1]);
+ }
+
+ function scrollIntoViewIfNeeded(el) {
+ const pr = el.parentElement.getBoundingClientRect();
+ const cr = el.getBoundingClientRect();
+ if (cr.top < pr.top) {
+ el.scrollIntoView(true);
+ } else if (cr.bottom > pr.bottom) {
+ el.scrollIntoView(false);
+ }
+ }
+
+ function extractUrlString(url) {
+ let extracted = url.replace(/url\((.*)\)/, '$1');
+
+ // In newer versions of chrome, the contents of url() will be quoted. Remove
+ // these quotes as well. If quotes are not present, match will fail and this
+ // becomes a no-op.
+ extracted = extracted.replace(/\"(.*)\"/, '$1');
+
+ return extracted;
+ }
+
+ function toThreeDigitLocaleString(value) {
+ return value.toLocaleString(
+ undefined, {minimumFractionDigits: 3, maximumFractionDigits: 3});
+ }
+
+ /**
+ * Returns true if |name| is the name of an unknown HTML element. Registered
+ * polymer elements are known, so this returns false. Typos of registered
+ * polymer element names are unknown, so this returns true for typos.
+ *
+ * @return {boolean}
+ */
+ function isUnknownElementName(name) {
+ return document.createElement(name) instanceof HTMLUnknownElement;
+ }
+
+ return {
+ isUnknownElementName,
+ toThreeDigitLocaleString,
+ instantiateTemplate,
+ windowRectForElement,
+ scrollIntoViewIfNeeded,
+ extractUrlString,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html b/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html
new file mode 100644
index 00000000000..4ab76e1d221
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/base/utils_test.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<dom-module id='instantiate-template-polymer-element-test'>
+ <template></template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'instantiate-template-polymer-element-test',
+ testProperty: 'Test'
+});
+</script>
+<template id="instantiate-template-polymer-test">
+ <instantiate-template-polymer-element-test>
+ </instantiate-template-polymer-element-test>
+</template>
+
+<template id="multiple-template-test">
+ <template>
+ <instantiate-template-polymer-element-test>
+ </instantiate-template-polymer-element-test>
+ <span test-attribute='TestAttribute'>Foo</span>
+ </template>
+ <instantiate-template-polymer-element-test>
+ </instantiate-template-polymer-element-test>
+</template>
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ test('instantiateTemplatePolymer', function() {
+ const e = tr.ui.b.instantiateTemplate(
+ '#instantiate-template-polymer-test',
+ THIS_DOC);
+ assert.strictEqual(e.children.length, 1);
+ assert.strictEqual(e.children[0].testProperty, 'Test');
+ });
+
+ test('instantiateTemplateMultipleTemplates', function() {
+ const outerElement = tr.ui.b.instantiateTemplate(
+ '#multiple-template-test',
+ THIS_DOC);
+ assert.strictEqual(outerElement.children.length, 2);
+ assert.strictEqual(outerElement.children[1].testProperty, 'Test');
+
+ // Make sure we can still instantiate inner templates, if we need them.
+ const innerElement = THIS_DOC.importNode(
+ outerElement.children[0].content, true);
+ assert.strictEqual(innerElement.children.length, 2);
+ assert.strictEqual(innerElement.children[0].testProperty, 'Test');
+ assert.strictEqual(
+ innerElement.children[1].getAttribute('test-attribute'),
+ 'TestAttribute');
+ assert.strictEqual(
+ Polymer.dom(innerElement.children[1]).textContent, 'Foo');
+ });
+
+ test('extractUrlStringAcceptsBothVersions', function() {
+ const oldStyleUrl = 'url(content)';
+ const newStyleUrl = 'url("content")';
+ const expectedResult = 'content';
+
+ assert.strictEqual(tr.ui.b.extractUrlString(oldStyleUrl), expectedResult);
+ assert.strictEqual(tr.ui.b.extractUrlString(newStyleUrl), expectedResult);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html
new file mode 100644
index 00000000000..f88e7a9bca3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state.html
@@ -0,0 +1,280 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/guid.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.b', function() {
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+
+ function BrushingState() {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.selection_ = new EventSet();
+ this.findMatches_ = new EventSet();
+ this.analysisViewRelatedEvents_ = new EventSet();
+ this.analysisLinkHoveredEvents_ = new EventSet();
+ this.appliedToModel_ = undefined;
+ this.viewSpecificBrushingStates_ = {};
+ }
+ BrushingState.prototype = {
+ get guid() {
+ return this.guid_;
+ },
+
+ clone() {
+ const that = new BrushingState();
+ that.selection_ = this.selection_;
+ that.findMatches_ = this.findMatches_;
+ that.analysisViewRelatedEvents_ = this.analysisViewRelatedEvents_;
+ that.analysisLinkHoveredEvents_ = this.analysisLinkHoveredEvents_;
+ that.viewSpecificBrushingStates_ = this.viewSpecificBrushingStates_;
+
+ return that;
+ },
+
+ equals(that) {
+ if (!this.selection_.equals(that.selection_)) {
+ return false;
+ }
+ if (!this.findMatches_.equals(that.findMatches_)) {
+ return false;
+ }
+ if (!this.analysisViewRelatedEvents_.equals(
+ that.analysisViewRelatedEvents_)) {
+ return false;
+ }
+ if (!this.analysisLinkHoveredEvents_.equals(
+ that.analysisLinkHoveredEvents_)) {
+ return false;
+ }
+ // We currently do not take the view-specific brushing states into
+ // account. If we did, every change of the view-specific brushing state
+ // of any view would cause a redraw of the whole UI (see the
+ // BrushingStateController.currentBrushingState setter).
+ return true;
+ },
+
+ get selectionOfInterest() {
+ if (this.selection_.length) {
+ return this.selection_;
+ }
+
+ if (this.highlight_.length) {
+ return this.highlight_;
+ }
+
+ if (this.analysisViewRelatedEvents_.length) {
+ return this.analysisViewRelatedEvents_;
+ }
+
+ if (this.analysisLinkHoveredEvents_.length) {
+ return this.analysisLinkHoveredEvents_;
+ }
+
+ return this.selection_;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ if (this.appliedToModel_) {
+ throw new Error('Cannot mutate this state right now');
+ }
+ if (selection === undefined) {
+ selection = new EventSet();
+ }
+ this.selection_ = selection;
+ },
+
+ get findMatches() {
+ return this.findMatches_;
+ },
+
+ set findMatches(findMatches) {
+ if (this.appliedToModel_) {
+ throw new Error('Cannot mutate this state right now');
+ }
+ if (findMatches === undefined) {
+ findMatches = new EventSet();
+ }
+ this.findMatches_ = findMatches;
+ },
+
+ get analysisViewRelatedEvents() {
+ return this.analysisViewRelatedEvents_;
+ },
+
+ set analysisViewRelatedEvents(analysisViewRelatedEvents) {
+ if (this.appliedToModel_) {
+ throw new Error('Cannot mutate this state right now');
+ }
+ if (!(analysisViewRelatedEvents instanceof EventSet)) {
+ analysisViewRelatedEvents = new EventSet();
+ }
+ this.analysisViewRelatedEvents_ = analysisViewRelatedEvents;
+ },
+
+ get analysisLinkHoveredEvents() {
+ return this.analysisLinkHoveredEvents_;
+ },
+
+ set analysisLinkHoveredEvents(analysisLinkHoveredEvents) {
+ if (this.appliedToModel_) {
+ throw new Error('Cannot mutate this state right now');
+ }
+ if (!(analysisLinkHoveredEvents instanceof EventSet)) {
+ analysisLinkHoveredEvents = new EventSet();
+ }
+ this.analysisLinkHoveredEvents_ = analysisLinkHoveredEvents;
+ },
+
+ get isAppliedToModel() {
+ return this.appliedToModel_ !== undefined;
+ },
+
+ get viewSpecificBrushingStates() {
+ return this.viewSpecificBrushingStates_;
+ },
+
+ set viewSpecificBrushingStates(viewSpecificBrushingStates) {
+ this.viewSpecificBrushingStates_ = viewSpecificBrushingStates;
+ },
+
+ get dimmedEvents_() {
+ const dimmedEvents = new EventSet();
+ dimmedEvents.addEventSet(this.findMatches);
+ dimmedEvents.addEventSet(this.analysisViewRelatedEvents_);
+ return dimmedEvents;
+ },
+
+ get brightenedEvents_() {
+ const brightenedEvents = new EventSet();
+ brightenedEvents.addEventSet(this.selection_);
+ brightenedEvents.addEventSet(this.analysisLinkHoveredEvents_);
+ return brightenedEvents;
+ },
+
+ /**
+ * This function sets the SelectionStates according to these rules:
+ *
+ * - Events in ONE of findMatches or analysisViewRelatedEvents
+ * are set to SelectionState.BRIGHTENED0.
+ * - Events in BOTH of findMatches and analysisViewRelatedEvents
+ * are set to SelectionState.BRIGHTENED1.
+ * - Events in ONE of selection or analysisLinkHoveredEvents
+ * are set to SelectionState.DIMMED1.
+ * - Events in BOTH selection and analysisLinkHoveredEvents
+ * are set to SelectionState.DIMMED2.
+ * - Events not in any of the above are set to SelectionState.NONE
+ * if there are no events in selection or analysisLinkHoveredEvents
+ * (i.e. model is "default bright") or SelectionState.DIMMED0 (i.e.
+ * model is "default dimmed").
+ *
+ * It is up to the caller to assure that all of the SelectionStates
+ * are the same before calling this function. Normally,
+ * this is done by calling unapplyFromModelSelectionState on the
+ * old brushing state first.
+ */
+ applyToEventSelectionStates(model) {
+ this.appliedToModel_ = model;
+
+ const dimmedEvents = this.dimmedEvents_;
+
+ // It's possible for this to get called with an undefined model pointer.
+ // If so, skip adjusting the defaults.
+ if (model) {
+ const newDefaultState = (
+ dimmedEvents.length ? SelectionState.DIMMED0 : SelectionState.NONE);
+
+ // Since all the states are the same, we can get the current default
+ // state by looking at the first element.
+ const currentDefaultState = tr.b.getFirstElement(
+ model.getDescendantEvents()).selectionState;
+
+ // If the default state was changed, then we have to iterate through
+ // and reset all the events to the new default state.
+ if (currentDefaultState !== newDefaultState) {
+ for (const e of model.getDescendantEvents()) {
+ e.selectionState = newDefaultState;
+ }
+ }
+ }
+
+ // Now we apply the other rules above.
+ let score;
+ for (const e of dimmedEvents) {
+ score = 0;
+ if (this.findMatches_.contains(e)) {
+ score++;
+ }
+ if (this.analysisViewRelatedEvents_.contains(e)) {
+ score++;
+ }
+ e.selectionState = SelectionState.getFromDimmingLevel(score);
+ }
+
+ for (const e of this.brightenedEvents_) {
+ score = 0;
+ if (this.selection_.contains(e)) {
+ score++;
+ }
+ if (this.analysisLinkHoveredEvents_.contains(e)) {
+ score++;
+ }
+ e.selectionState = SelectionState.getFromBrighteningLevel(score);
+ }
+ },
+
+ transferModelOwnershipToClone(that) {
+ if (!this.appliedToModel_) {
+ throw new Error('Not applied');
+ }
+ // Assumes this.equals(that).
+ that.appliedToModel_ = this.appliedToModel_;
+ this.appliedToModel_ = undefined;
+ },
+
+ /**
+ * Unapplies this brushing state from the model selection state.
+ * Resets all the SelectionStates to their default value (DIMMED0 or NONE)
+ * and returns the default selection states. The caller should store this
+ * value and pass it into applyFromModelSelectionStat when that is called.
+ */
+ unapplyFromEventSelectionStates() {
+ if (!this.appliedToModel_) {
+ throw new Error('Not applied');
+ }
+ const model = this.appliedToModel_;
+ this.appliedToModel_ = undefined;
+
+ const dimmedEvents = this.dimmedEvents_;
+ const defaultState = (
+ dimmedEvents.length ? SelectionState.DIMMED0 : SelectionState.NONE);
+
+ for (const e of this.brightenedEvents_) {
+ e.selectionState = defaultState;
+ }
+ for (const e of dimmedEvents) {
+ e.selectionState = defaultState;
+ }
+ return defaultState;
+ }
+ };
+
+ return {
+ BrushingState,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html
new file mode 100644
index 00000000000..6a547c339c3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller.html
@@ -0,0 +1,317 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/base/ui_state.html">
+<link rel="import" href="/tracing/ui/brushing_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.c', function() {
+ const BrushingState = tr.ui.b.BrushingState;
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ function BrushingStateController(timelineView) {
+ tr.b.EventTarget.call(this);
+
+ this.timelineView_ = timelineView;
+ this.currentBrushingState_ = new BrushingState();
+
+ this.onPopState_ = this.onPopState_.bind(this);
+ this.historyEnabled_ = false;
+ this.selections_ = {};
+ }
+
+ BrushingStateController.prototype = {
+ __proto__: tr.b.EventTarget.prototype,
+
+ dispatchChangeEvent_() {
+ const e = new tr.b.Event('change', false, false);
+ this.dispatchEvent(e);
+ },
+
+ get model() {
+ if (!this.timelineView_) {
+ return undefined;
+ }
+ return this.timelineView_.model;
+ },
+
+ get trackView() {
+ if (!this.timelineView_) {
+ return undefined;
+ }
+ return this.timelineView_.trackView;
+ },
+
+ get viewport() {
+ if (!this.timelineView_) {
+ return undefined;
+ }
+ if (!this.timelineView_.trackView) {
+ return undefined;
+ }
+ return this.timelineView_.trackView.viewport;
+ },
+
+ /* History system */
+ get historyEnabled() {
+ return this.historyEnabled_;
+ },
+
+ set historyEnabled(historyEnabled) {
+ this.historyEnabled_ = !!historyEnabled;
+ if (historyEnabled) {
+ window.addEventListener('popstate', this.onPopState_);
+ } else {
+ window.removeEventListener('popstate', this.onPopState_);
+ }
+ },
+
+ modelWillChange() {
+ if (this.currentBrushingState_.isAppliedToModel) {
+ this.currentBrushingState_.unapplyFromEventSelectionStates();
+ }
+ },
+
+ modelDidChange() {
+ this.selections_ = {};
+
+ this.currentBrushingState_ = new BrushingState();
+ this.currentBrushingState_.applyToEventSelectionStates(this.model);
+
+ const e = new tr.b.Event('model-changed', false, false);
+ this.dispatchEvent(e);
+
+ this.dispatchChangeEvent_();
+ },
+
+ onUserInitiatedSelectionChange_() {
+ const selection = this.selection;
+ if (this.historyEnabled) {
+ // Save the selection so that when back button is pressed,
+ // it could be retrieved.
+ this.selections_[selection.guid] = selection;
+ const state = {
+ selection_guid: selection.guid
+ };
+
+ window.history.pushState(state, document.title);
+ }
+ },
+
+ onPopState_(e) {
+ if (e.state === null) return;
+
+ const selection = this.selections_[e.state.selection_guid];
+ if (selection) {
+ const newState = this.currentBrushingState_.clone();
+ newState.selection = selection;
+ this.currentBrushingState = newState;
+ }
+ e.stopPropagation();
+ },
+
+ get selection() {
+ return this.currentBrushingState_.selection;
+ },
+ get findMatches() {
+ return this.currentBrushingState_.findMatches;
+ },
+
+ get selectionOfInterest() {
+ return this.currentBrushingState_.selectionOfInterest;
+ },
+
+ get currentBrushingState() {
+ return this.currentBrushingState_;
+ },
+
+ set currentBrushingState(newBrushingState) {
+ if (newBrushingState.isAppliedToModel) {
+ throw new Error('Cannot apply this state, it is applied');
+ }
+
+ // This function uses value-equality on the states so that state can
+ // changed to a clone of itself without causing a change event, while
+ // still having the actual state object change to the new clone.
+ const hasValueChanged = !this.currentBrushingState_.equals(
+ newBrushingState);
+
+ if (newBrushingState !== this.currentBrushingState_ && !hasValueChanged) {
+ if (this.currentBrushingState_.isAppliedToModel) {
+ this.currentBrushingState_.transferModelOwnershipToClone(
+ newBrushingState);
+ }
+ this.currentBrushingState_ = newBrushingState;
+ return;
+ }
+
+ if (this.currentBrushingState_.isAppliedToModel) {
+ this.currentBrushingState_.unapplyFromEventSelectionStates();
+ }
+
+ this.currentBrushingState_ = newBrushingState;
+
+ this.currentBrushingState_.applyToEventSelectionStates(this.model);
+
+ this.dispatchChangeEvent_();
+ },
+
+ /**
+ * @param {Filter} filter The filter to use for finding matches.
+ * @param {Selection} selection The selection to add matches to.
+ * @return {Task} which performs the filtering.
+ */
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ const timelineView = this.timelineView_.trackView;
+ if (!timelineView) {
+ return new tr.b.Task();
+ }
+ return timelineView.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, selection);
+ },
+
+ findTextChangedTo(allPossibleMatches) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.findMatches = allPossibleMatches;
+ this.currentBrushingState = newBrushingState;
+ },
+
+ findFocusChangedTo(currentFocus) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = currentFocus;
+ this.currentBrushingState = newBrushingState;
+
+ this.onUserInitiatedSelectionChange_();
+ },
+
+ findTextCleared() {
+ if (this.xNavStringMarker_ !== undefined) {
+ this.model.removeAnnotation(this.xNavStringMarker_);
+ this.xNavStringMarker_ = undefined;
+ }
+
+ if (this.guideLineAnnotation_ !== undefined) {
+ this.model.removeAnnotation(this.guideLineAnnotation_);
+ this.guideLineAnnotation_ = undefined;
+ }
+
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = new EventSet();
+ newBrushingState.findMatches = new EventSet();
+ this.currentBrushingState = newBrushingState;
+
+ this.onUserInitiatedSelectionChange_();
+ },
+
+ uiStateFromString(string) {
+ return tr.ui.b.UIState.fromUserFriendlyString(
+ this.model, this.viewport, string);
+ },
+
+ navToPosition(uiState, showNavLine) {
+ this.trackView.navToPosition(uiState, showNavLine);
+ },
+
+ changeSelectionFromTimeline(selection) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = selection;
+ newBrushingState.findMatches = new EventSet();
+ this.currentBrushingState = newBrushingState;
+
+ this.onUserInitiatedSelectionChange_();
+ },
+
+ showScriptControlSelection(selection) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = selection;
+ newBrushingState.findMatches = new EventSet();
+ this.currentBrushingState = newBrushingState;
+ },
+
+ changeSelectionFromRequestSelectionChangeEvent(selection) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.selection = selection;
+ newBrushingState.findMatches = new EventSet();
+ this.currentBrushingState = newBrushingState;
+
+ this.onUserInitiatedSelectionChange_();
+ },
+
+ changeAnalysisViewRelatedEvents(eventSet) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.analysisViewRelatedEvents = eventSet;
+ this.currentBrushingState = newBrushingState;
+ },
+
+ changeAnalysisLinkHoveredEvents(eventSet) {
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.analysisLinkHoveredEvents = eventSet;
+ this.currentBrushingState = newBrushingState;
+ },
+
+ getViewSpecificBrushingState(viewId) {
+ return this.currentBrushingState.viewSpecificBrushingStates[viewId];
+ },
+
+ changeViewSpecificBrushingState(viewId, newState) {
+ const oldStates = this.currentBrushingState_.viewSpecificBrushingStates;
+ const newStates = {};
+ for (const id in oldStates) {
+ newStates[id] = oldStates[id];
+ }
+ if (newState === undefined) {
+ delete newStates[viewId];
+ } else {
+ newStates[viewId] = newState;
+ }
+
+ const newBrushingState = this.currentBrushingState_.clone();
+ newBrushingState.viewSpecificBrushingStates = newStates;
+ this.currentBrushingState = newBrushingState;
+ }
+ };
+
+ BrushingStateController.getControllerForElement = function(element) {
+ if (tr.isHeadless) {
+ throw new Error('Unsupported');
+ }
+ let currentElement = element;
+ while (currentElement) {
+ if (currentElement.brushingStateController) {
+ return currentElement.brushingStateController;
+ }
+
+ // Walk up the DOM.
+ if (currentElement.parentElement) {
+ currentElement = currentElement.parentElement;
+ continue;
+ }
+
+ // Possibly inside a shadow DOM.
+ let currentNode = currentElement;
+ while (Polymer.dom(currentNode).parentNode) {
+ currentNode = Polymer.dom(currentNode).parentNode;
+ }
+ currentElement = currentNode.host;
+ }
+ return undefined;
+ };
+
+ return {
+ BrushingStateController,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html
new file mode 100644
index 00000000000..b72aeccf16a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_controller_test.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Task = tr.b.Task;
+
+ function newSimpleFakeTimelineView() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5}));
+ m.sB = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15}));
+ m.sC = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20}));
+ });
+
+ // Fake timeline view. So fake its ... just wow.
+ const timelineView = {
+ model: m
+ };
+ return timelineView;
+ }
+
+ function doesCauseChangeToFire(brushingStateController, cb, opt_this) {
+ let didFire = false;
+ function didFireCb() {
+ didFire = true;
+ }
+ brushingStateController.addEventListener('change', didFireCb);
+ cb.call(opt_this);
+ brushingStateController.removeEventListener('change', didFireCb);
+ return didFire;
+ }
+
+ test('simpleStateChanges', function() {
+ const timelineView = newSimpleFakeTimelineView();
+ const brushingStateController =
+ new tr.c.BrushingStateController(timelineView);
+ const m = timelineView.model;
+
+ // Setting empty brushing state doesn't cause change event. This behavior
+ // is triggered when the user tries to search for something when no trace
+ // has been loaded yet in chrome://tracing.
+ const bs0 = new tr.ui.b.BrushingState();
+ assert.isFalse(doesCauseChangeToFire(
+ brushingStateController,
+ function() {
+ brushingStateController.currentBrushingState = bs0;
+ }));
+ assert.isFalse(bs0.isAppliedToModel);
+ assert.strictEqual(brushingStateController.currentBrushingState, bs0);
+
+ // Setting causes change.
+ const bs1 = new tr.ui.b.BrushingState();
+ bs1.selection = new EventSet([m.sA]);
+ assert.isTrue(doesCauseChangeToFire(
+ brushingStateController,
+ function() {
+ brushingStateController.currentBrushingState = bs1;
+ }));
+ assert.isTrue(bs1.isAppliedToModel);
+
+ // Setting value equivalent doesn't cause change event.
+ const bs2 = bs1.clone();
+ assert.isFalse(doesCauseChangeToFire(
+ brushingStateController,
+ function() {
+ brushingStateController.currentBrushingState = bs2;
+ }));
+ assert.strictEqual(brushingStateController.currentBrushingState, bs2);
+ assert.isTrue(
+ brushingStateController.currentBrushingState.isAppliedToModel);
+
+ // Setting to something different unapplies the old bs.
+ const bs3 = new tr.ui.b.BrushingState();
+ bs3.findMatches = new EventSet([m.sA, m.sB]);
+ brushingStateController.currentBrushingState = bs3;
+ assert.isTrue(bs3.isAppliedToModel);
+ assert.isFalse(bs2.isAppliedToModel);
+ });
+
+ test('modelCausesStateChange', function() {
+ const timelineView = newSimpleFakeTimelineView();
+ const brushingStateController =
+ new tr.c.BrushingStateController(timelineView);
+
+ const m1 = timelineView.model;
+
+ const bs1 = new tr.ui.b.BrushingState();
+ bs1.selection = new EventSet([m1.sA]);
+
+ // Change the model.
+ const m2 = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5}));
+ });
+ assert.isTrue(doesCauseChangeToFire(
+ brushingStateController,
+ function() {
+ brushingStateController.modelWillChange();
+ timelineView.model = m2;
+ brushingStateController.modelDidChange();
+ }));
+ assert.strictEqual(
+ brushingStateController.currentBrushingState.selection.length, 0);
+ });
+
+ function addChildDiv(element) {
+ const child = element.ownerDocument.createElement('div');
+ Polymer.dom(element).appendChild(child);
+ return child;
+ }
+
+ function addShadowChildDiv(element) {
+ const shadowRoot = element.createShadowRoot();
+ return addChildDiv(shadowRoot);
+ }
+
+ if (!tr.isHeadless) {
+ test('getControllerForElement_none', function() {
+ const element = document.createElement('div');
+
+ assert.isUndefined(
+ tr.c.BrushingStateController.getControllerForElement(element));
+ });
+
+ test('getControllerForElement_self', function() {
+ const controller = new tr.c.BrushingStateController(undefined);
+ const element = document.createElement('div');
+ element.brushingStateController = controller;
+
+ assert.strictEqual(
+ tr.c.BrushingStateController.getControllerForElement(element),
+ controller);
+ });
+
+ test('getControllerForElement_ancestor', function() {
+ const controller = new tr.c.BrushingStateController(undefined);
+ const ancestor = document.createElement('div');
+ ancestor.brushingStateController = controller;
+
+ const element = addChildDiv(addChildDiv(addChildDiv(ancestor)));
+ assert.strictEqual(
+ tr.c.BrushingStateController.getControllerForElement(element),
+ controller);
+ });
+
+ test('getControllerForElement_host', function() {
+ const controller = new tr.c.BrushingStateController(undefined);
+ const host = document.createElement('div');
+ host.brushingStateController = controller;
+
+ const element = addShadowChildDiv(host);
+ assert.strictEqual(
+ tr.c.BrushingStateController.getControllerForElement(element),
+ controller);
+ });
+
+ test('getControllerForElement_hierarchy', function() {
+ const controller1 = new tr.c.BrushingStateController(undefined);
+ const root = document.createElement('div');
+ root.brushingStateController = controller1;
+
+ const controller2 = new tr.c.BrushingStateController(undefined);
+ const child = addChildDiv(root);
+ child.brushingStateController = controller2;
+
+ const controller3 = new tr.c.BrushingStateController(undefined);
+ const shadowChild = addShadowChildDiv(child);
+ shadowChild.brushingStateController = controller3;
+
+ const element = addChildDiv(addChildDiv(addShadowChildDiv(
+ addChildDiv(addChildDiv(addShadowChildDiv(
+ addChildDiv(shadowChild)))))));
+ assert.strictEqual(
+ tr.c.BrushingStateController.getControllerForElement(element),
+ controller3);
+ });
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html
new file mode 100644
index 00000000000..aa33b12d116
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/brushing_state_test.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+
+ function newSimpleModel() {
+ return tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.sA = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'a', start: 0, end: 5}));
+ m.sB = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'b', start: 10, end: 15}));
+ m.sC = m.t2.sliceGroup.pushSlice(
+ newSliceEx({title: 'c', start: 20, end: 20}));
+ });
+ }
+
+ test('brushingStateSimple', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+
+ bs.applyToEventSelectionStates(m);
+ assert.strictEqual(m.sA.selectionState, SelectionState.SELECTED);
+ bs.unapplyFromEventSelectionStates();
+ assert.strictEqual(m.sA.selectionState, SelectionState.NONE);
+ });
+
+
+ test('selectionAndAnalysisHover', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+ bs.analysisLinkHoveredEvents = new EventSet([m.sA, m.sB]);
+
+ bs.applyToEventSelectionStates(m);
+ assert.strictEqual(m.sA.selectionState, SelectionState.BRIGHTENED1);
+ assert.strictEqual(m.sB.selectionState, SelectionState.BRIGHTENED0);
+ bs.unapplyFromEventSelectionStates();
+ assert.strictEqual(m.sA.selectionState, SelectionState.NONE);
+ });
+
+ test('brushingStateWithFindMatches', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+ bs.findMatches = new EventSet([m.sA, m.sB]);
+
+ bs.applyToEventSelectionStates(m);
+ assert.strictEqual(m.sA.selectionState, SelectionState.BRIGHTENED0);
+ assert.strictEqual(m.sB.selectionState, SelectionState.DIMMED1);
+ assert.strictEqual(m.sC.selectionState, SelectionState.DIMMED0);
+ bs.unapplyFromEventSelectionStates();
+ assert.strictEqual(m.sA.selectionState, SelectionState.DIMMED0);
+ assert.strictEqual(m.sB.selectionState, SelectionState.DIMMED0);
+ assert.strictEqual(m.sC.selectionState, SelectionState.DIMMED0);
+ });
+
+ test('brushingTransfer', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+
+ const bs2 = bs.clone();
+
+ bs.applyToEventSelectionStates(m);
+ assert.strictEqual(m.sA.selectionState, SelectionState.SELECTED);
+ bs.transferModelOwnershipToClone(bs2);
+ assert.isFalse(bs.isAppliedToModel);
+ assert.isTrue(bs2.isAppliedToModel);
+
+ bs2.unapplyFromEventSelectionStates();
+ assert.strictEqual(m.sA.selectionState, SelectionState.NONE);
+ assert.strictEqual(m.sB.selectionState, SelectionState.NONE);
+ assert.strictEqual(m.sC.selectionState, SelectionState.NONE);
+ });
+
+ test('equality', function() {
+ const m = newSimpleModel();
+
+ const bs = new tr.ui.b.BrushingState();
+ bs.selection = new EventSet([m.sA]);
+ bs.findMatches = new EventSet([m.sB]);
+ bs.applyToEventSelectionStates = new EventSet([m.sC]);
+
+ // Clone equality, but with shared refs.
+ const bs2 = bs.clone();
+ assert.isTrue(bs.equals(bs2));
+
+ // Same value, different refs.
+ bs2.selection = new EventSet([m.sA]);
+ assert.isTrue(bs.equals(bs2));
+
+ // Different actual values.
+ bs2.selection = new EventSet([m.sB]);
+ assert.isFalse(bs.equals(bs2));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html
new file mode 100644
index 00000000000..8e579d90921
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="stylesheet" href="/tracing/ui/extras/about_tracing/common.css">
+<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ window.profilingView = undefined; // Made global for debugging purposes only.
+
+ document.addEventListener('DOMContentLoaded', function() {
+ window.profilingView = new tr.ui.e.about_tracing.ProfilingView();
+ profilingView.timelineView.globalMode = true;
+ Polymer.dom(document.body).appendChild(profilingView);
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css
new file mode 100644
index 00000000000..3db21b67ffa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css
@@ -0,0 +1,25 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+html,
+body {
+ height: 100%;
+}
+
+body {
+ flex-direction: column;
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+
+body > x-profiling-view {
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
+body > x-profiling-view > x-timeline-view:focus {
+ outline: 0
+}
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html
new file mode 100644
index 00000000000..fa348a7b661
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/base64.html">
+<script>
+
+'use strict';
+
+/**
+ * A devtools protocol stream object.
+ *
+ * This reads a stream of data over the remote debugging connection.
+ */
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ class DevtoolsStream {
+ constructor(connection, streamHandle) {
+ this.connection_ = connection;
+ this.streamHandle_ = streamHandle;
+ this.closed_ = false;
+ }
+
+ async read() {
+ if (this.closed_) {
+ throw new Error('stream is closed');
+ }
+
+ const pendingRequests = [];
+
+ const READ_REQUEST_BYTES = 32768;
+ const makeRequest = () => {
+ pendingRequests.push(this.connection_.req(
+ 'IO.read',
+ {
+ handle: this.streamHandle_,
+ size: READ_REQUEST_BYTES,
+ }));
+ };
+
+ const MAX_CONCURRENT_REQUESTS = 2;
+ for (let i = 0; i < MAX_CONCURRENT_REQUESTS; ++i) {
+ makeRequest();
+ }
+
+ const chunks = [];
+ let base64 = false;
+ while (true) {
+ const request = pendingRequests.shift();
+ const response = await request;
+
+ chunks.push(response.data);
+ if (response.base64Encoded) {
+ base64 = true;
+ }
+ if (response.eof) {
+ break;
+ }
+
+ makeRequest();
+ }
+
+ if (base64) {
+ let totalSize = 0;
+ for (const chunk of chunks) {
+ totalSize += tr.b.Base64.getDecodedBufferLength(chunk);
+ }
+ const buffer = new ArrayBuffer(totalSize);
+ let offset = 0;
+ for (const chunk of chunks) {
+ offset += tr.b.Base64.DecodeToTypedArray(
+ chunk,
+ new DataView(buffer, offset));
+ }
+ return buffer;
+ }
+
+ return chunks.join('');
+ }
+
+ close() {
+ this.closed_ = true;
+ return this.connection_.req('IO.close', { handle: this.streamHandle_ });
+ }
+
+ async readAndClose() {
+ const data = await this.read();
+ this.close();
+ return data;
+ }
+ }
+
+ return {
+ DevtoolsStream,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html
new file mode 100644
index 00000000000..791f5e77705
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<script>
+
+'use strict';
+
+/**
+ * Contains connection code that inspector's embedding framework calls on
+ * tracing, and that tracing can use to talk to inspector.
+ */
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ class InspectorConnection {
+ constructor(windowGlobal) {
+ if (!windowGlobal.DevToolsHost) {
+ throw new Error('Requires window.DevToolsHost');
+ }
+ this.devToolsHost_ = windowGlobal.DevToolsHost;
+ this.installDevToolsAPI_(windowGlobal);
+
+ this.nextRequestId_ = 1;
+ this.pendingRequestResolversId_ = {};
+
+ this.notificationListenersByMethodName_ = {};
+ }
+
+ req(method, params) {
+ const id = this.nextRequestId_++;
+ const msg = JSON.stringify({
+ id,
+ method,
+ params
+ });
+ const devtoolsMessageStr = JSON.stringify(
+ {id, 'method': 'dispatchProtocolMessage', 'params': [msg]});
+ this.devToolsHost_.sendMessageToEmbedder(devtoolsMessageStr);
+
+ return new Promise(function(resolve, reject) {
+ this.pendingRequestResolversId_[id] = {
+ resolve,
+ reject
+ };
+ }.bind(this));
+ }
+
+ setNotificationListener(method, listener) {
+ this.notificationListenersByMethodName_[method] = listener;
+ }
+
+ dispatchMessage_(payload) {
+ const isStringPayload = typeof payload === 'string';
+ // Special handling for Tracing.dataCollected because it is high
+ // bandwidth.
+ const isDataCollectedMessage = isStringPayload ?
+ payload.includes('"method": "Tracing.dataCollected"') :
+ payload.method === 'Tracing.dataCollected';
+ if (isDataCollectedMessage) {
+ const listener = this.notificationListenersByMethodName_[
+ 'Tracing.dataCollected'];
+ if (listener) {
+ // FIXME(loislo): trace viewer should be able to process
+ // raw message object because string based version a few times
+ // slower on the browser side.
+ // see https://codereview.chromium.org/784513002.
+ listener(isStringPayload ? payload : JSON.stringify(payload));
+ return;
+ }
+ }
+
+ const message = isStringPayload ? JSON.parse(payload) : payload;
+ if (message.id) {
+ const resolver = this.pendingRequestResolversId_[message.id];
+ if (resolver === undefined) {
+ return;
+ }
+ if (message.error) {
+ resolver.reject(message.error);
+ return;
+ }
+ resolver.resolve(message.result);
+ return;
+ }
+
+ if (message.method) {
+ const listener = this.notificationListenersByMethodName_[
+ message.method];
+ if (listener === undefined) return;
+ listener(message.params);
+ return;
+ }
+ }
+
+ installDevToolsAPI_(windowGlobal) {
+ // Interface used by inspector when it hands data to us from the backend.
+ windowGlobal.DevToolsAPI = {
+ setToolbarColors() { },
+ addExtensions() { },
+ setInspectedPageId() { },
+ dispatchMessage: this.dispatchMessage_.bind(this),
+ };
+
+ // Temporary until inspector backend switches to DevToolsAPI.
+ windowGlobal.InspectorFrontendAPI = windowGlobal.DevToolsAPI;
+ }
+ }
+
+ return {
+ InspectorConnection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html
new file mode 100644
index 00000000000..ac5afabae12
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html
@@ -0,0 +1,216 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/extras/about_tracing/devtools_stream.html">
+<link rel="import" href="/tracing/ui/extras/about_tracing/inspector_connection.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ function createResolvedPromise(data) {
+ const promise = new Promise(function(resolve, reject) {
+ if (data) {
+ resolve(data);
+ } else {
+ resolve();
+ }
+ });
+ return promise;
+ }
+
+ function appendTraceChunksTo(chunks, messageString) {
+ if (typeof messageString !== 'string') {
+ throw new Error('Invalid data');
+ }
+ const re = /"params":\s*\{\s*"value":\s*\[([^]+)\]\s*\}\s*\}/;
+ const m = re.exec(messageString);
+ if (!m) {
+ throw new Error('Malformed response');
+ }
+
+ if (chunks.length > 1) {
+ chunks.push(',');
+ }
+ chunks.push(m[1]);
+ }
+
+ /**
+ * Controls tracing using the inspector's FrontendAgentHost APIs.
+ */
+ class InspectorTracingControllerClient extends
+ tr.ui.e.about_tracing.TracingControllerClient {
+ constructor(connection) {
+ super();
+ this.recording_ = false;
+ this.bufferUsage_ = 0;
+ this.conn_ = connection;
+ this.currentTraceTextChunks_ = undefined;
+ }
+
+ beginMonitoring(monitoringOptions) {
+ throw new Error('Not implemented');
+ }
+
+ endMonitoring() {
+ throw new Error('Not implemented');
+ }
+
+ captureMonitoring() {
+ throw new Error('Not implemented');
+ }
+
+ getMonitoringStatus() {
+ return createResolvedPromise({
+ isMonitoring: false,
+ categoryFilter: '',
+ useSystemTracing: false,
+ useContinuousTracing: false,
+ useSampling: false
+ });
+ }
+
+ getCategories() {
+ const res = this.conn_.req('Tracing.getCategories', {});
+ return res.then(function(result) {
+ return result.categories;
+ }, function(err) {
+ return [];
+ });
+ }
+
+ beginRecording(recordingOptions) {
+ if (this.recording_) {
+ throw new Error('Already recording');
+ }
+ this.recording_ = 'starting';
+
+ // The devtools and tracing endpoints have slightly different parameter
+ // configurations. Noteably, recordMode has different spelling
+ // requirements.
+ function RewriteRecordMode(recordMode) {
+ if (recordMode === 'record-until-full') {
+ return 'recordUntilFull';
+ }
+ if (recordMode === 'record-continuously') {
+ return 'recordContinuously';
+ }
+ if (recordMode === 'record-as-much-as-possible') {
+ return 'recordAsMuchAsPossible';
+ }
+ return 'unsupported record mode';
+ }
+
+ const traceConfigStr = {
+ includedCategories: recordingOptions.included_categories,
+ excludedCategories: recordingOptions.excluded_categories,
+ recordMode: RewriteRecordMode(recordingOptions.record_mode),
+ enableSystrace: recordingOptions.enable_systrace
+ };
+ if ('memory_dump_config' in recordingOptions) {
+ traceConfigStr.memoryDumpConfig = recordingOptions.memory_dump_config;
+ }
+ let res = this.conn_.req(
+ 'Tracing.start',
+ {
+ traceConfig: traceConfigStr,
+ transferMode: 'ReturnAsStream',
+ streamCompression: 'gzip',
+ bufferUsageReportingInterval: 1000
+ });
+ res = res.then(
+ function ok() {
+ this.conn_.setNotificationListener(
+ 'Tracing.bufferUsage',
+ this.onBufferUsageUpdateFromInspector_.bind(this));
+ this.recording_ = true;
+ }.bind(this),
+ function error() {
+ this.recording_ = false;
+ }.bind(this));
+ return res;
+ }
+
+ onBufferUsageUpdateFromInspector_(params) {
+ this.bufferUsage_ = params.value || params.percentFull;
+ }
+
+ beginGetBufferPercentFull() {
+ return tr.b.timeout(100).then(() => this.bufferUsage_);
+ }
+
+ onDataCollected_(messageString) {
+ appendTraceChunksTo(this.currentTraceTextChunks_, messageString);
+ }
+
+ async endRecording() {
+ if (this.recording_ === false) {
+ return createResolvedPromise();
+ }
+
+ if (this.recording_ !== true) {
+ throw new Error('Cannot end');
+ }
+
+ this.currentTraceTextChunks_ = ['['];
+ const clearListeners = () => {
+ this.conn_.setNotificationListener(
+ 'Tracing.bufferUsage', undefined);
+ this.conn_.setNotificationListener(
+ 'Tracing.tracingComplete', undefined);
+ this.conn_.setNotificationListener(
+ 'Tracing.dataCollected', undefined);
+ };
+
+ try {
+ this.conn_.setNotificationListener(
+ 'Tracing.dataCollected', this.onDataCollected_.bind(this));
+
+ const tracingComplete = new Promise((resolve, reject) => {
+ this.conn_.setNotificationListener(
+ 'Tracing.tracingComplete', resolve);
+ });
+
+ this.recording_ = 'stopping';
+ await this.conn_.req('Tracing.end', {});
+ const params = await tracingComplete;
+
+ this.traceName_ = 'trace.json';
+ if ('stream' in params) {
+ const stream = new tr.ui.e.about_tracing.DevtoolsStream(
+ this.conn_, params.stream);
+ const streamCompression = params.streamCompression || 'none';
+ if (streamCompression === 'gzip') {
+ this.traceName_ = 'trace.json.gz';
+ }
+
+ return await stream.readAndClose();
+ }
+
+ this.currentTraceTextChunks_.push(']');
+ const traceText = this.currentTraceTextChunks_.join('');
+ this.currentTraceTextChunks_ = undefined;
+ return traceText;
+ } finally {
+ clearListeners();
+ this.recording_ = false;
+ }
+ }
+
+ defaultTraceName() {
+ return this.traceName_;
+ }
+ }
+
+ return {
+ InspectorTracingControllerClient,
+ appendTraceChunksTo,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html
new file mode 100644
index 00000000000..4a6585ac9e8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html
@@ -0,0 +1,396 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/inspector_connection.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html">
+
+<script>
+'use strict';
+
+function makeController() {
+ const controller =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient();
+ controller.conn_ = new (function() {
+ this.req = function(method, params) {
+ const msg = JSON.stringify({
+ id: 1,
+ method,
+ params
+ });
+ return new (function() {
+ this.msg = msg;
+ this.then = function(m1, m2) {
+ return this;
+ };
+ })();
+ };
+ this.setNotificationListener = function(method, listener) {
+ };
+ })();
+ return controller;
+}
+
+tr.b.unittest.testSuite(function() {
+ test('beginRecording_sendCategoriesAndOptions', function() {
+ const controller = makeController();
+
+ const recordingOptions = {
+ included_categories: ['a', 'b', 'c'],
+ excluded_categories: ['e'],
+ enable_systrace: false,
+ record_mode: 'record-until-full',
+ };
+
+ const result = JSON.parse(controller.beginRecording(recordingOptions).msg);
+ assert.deepEqual(
+ result.params.traceConfig.includedCategories, ['a', 'b', 'c']);
+ assert.deepEqual(
+ result.params.traceConfig.excludedCategories, ['e']);
+ assert.strictEqual(
+ result.params.traceConfig.recordMode, 'recordUntilFull');
+ assert.isFalse(
+ result.params.traceConfig.enableSystrace);
+ assert.isFalse('memoryDumpConfig' in result.params.traceConfig);
+ });
+
+ test('beginRecording_sendCategoriesAndOptionsWithMemoryInfra', function() {
+ const controller = makeController();
+
+ const memoryConfig = { triggers: [] };
+ memoryConfig.triggers.push(
+ {'mode': 'detailed', 'periodic_interval_ms': 10000});
+ const recordingOptions = {
+ included_categories: ['c', 'disabled-by-default-memory-infra', 'a'],
+ excluded_categories: ['e'],
+ enable_systrace: false,
+ record_mode: 'test-mode',
+ memory_dump_config: memoryConfig,
+ };
+
+ const result = JSON.parse(controller.beginRecording(recordingOptions).msg);
+ assert.isTrue(
+ result.params.traceConfig.memoryDumpConfig.triggers.length === 1);
+ assert.strictEqual(result.params.traceConfig.memoryDumpConfig.
+ triggers[0].mode, 'detailed');
+ assert.strictEqual(result.params.traceConfig.memoryDumpConfig.
+ triggers[0].periodic_interval_ms, 10000);
+ });
+
+ test('oldFormat', function() {
+ const chunks = [];
+ tr.ui.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [ {"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}} ] } }"'); // @suppress longLineCheck
+ assert.strictEqual(chunks.length, 1);
+ JSON.parse('[' + chunks.join('') + ']');
+ });
+
+ test('newFormat', function() {
+ const chunks = [];
+ tr.ui.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [{"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}}] } }"'); // @suppress longLineCheck
+ assert.strictEqual(chunks.length, 1);
+ JSON.parse('[' + chunks.join('') + ']');
+ });
+
+ test('stringAndObjectPayload', function() {
+ const connection =
+ new tr.ui.e.about_tracing.InspectorConnection({DevToolsHost: {}});
+ connection.setNotificationListener('Tracing.dataCollected',
+ function(message) {
+ assert.typeOf(message, 'string');
+ JSON.parse(message);
+ }
+ );
+ connection.dispatchMessage_('{ "method": "Tracing.dataCollected", "params": { "value": [] } }'); // @suppress longLineCheck
+ connection.dispatchMessage_({'method': 'Tracing.dataCollected', 'params': {'value': [] } }); // @suppress longLineCheck
+ });
+
+ // Makes a fake version of DevToolsHost, which is the object injected
+ // by the chrome inspector to allow tracing a remote instance of chrome.
+ //
+ // The fake host doesn't do much by itself - you have to install
+ // callbacks for incoming messages via handleMessage().
+ function makeFakeDevToolsHost() {
+ return new (function() {
+ this.pendingMethods_ = [];
+ this.messageHandlers_ = [];
+
+ // Sends a message to DevTools host. This is used by
+ // InspectorTracingControllerClient to communicate with the remote
+ // debugging tracing backend.
+ this.sendMessageToEmbedder = function(devtoolsMessageStr) {
+ this.pendingMethods_.push(JSON.parse(devtoolsMessageStr));
+ this.tryMessageHandlers_();
+ };
+
+ // Runs remote debugging message handlers. Handlers are installed
+ // by test code via handleMessage().
+ this.tryMessageHandlers_ = function() {
+ while (this.pendingMethods_.length !== 0) {
+ const message = this.pendingMethods_[0];
+ const params = JSON.parse(message.params);
+ let handled = false;
+ const handlersToRemove = [];
+
+ // Try to find a handler for this method.
+ for (const handler of this.messageHandlers_) {
+ if (handler(params, () => handlersToRemove.push(handler))) {
+ handled = true;
+ break;
+ }
+ }
+
+ // Remove any handlers that requested removal.
+ this.messageHandlers_ = this.messageHandlers_.filter(
+ (handler) => !handlersToRemove.includes(handler));
+
+ // Remove any handled messages.
+ if (handled) {
+ this.pendingMethods_.shift();
+ } else {
+ return; // Methods must be handled in order.
+ }
+ }
+ };
+
+ // Installs a message handler that will be invoked for each
+ // incoming message from InspectorTracingControllerClient.
+ //
+ // handleMessage((message, removeSelf) => {
+ // // Try to handle |message|.
+ // // Call |removeSelf| to remove this handler for future messages.
+ // // Return whether |message| was handled. Otherwise other handlers
+ // // will be run until one of them succeeds.
+ // }
+ this.handleMessage = function(handler) {
+ this.messageHandlers_.push(handler);
+ this.tryMessageHandlers_();
+ };
+
+ // Installs a message handler that will handle the first call to the named
+ // method. Returns a promise for the parameters passed to the method.
+ this.handleMethod = function(method) {
+ const result = new Promise((resolve, reject) => {
+ this.handleMessage(
+ (requestParams, removeHandler) => {
+ if (requestParams.method === method) {
+ removeHandler();
+ resolve(requestParams);
+ return true;
+ }
+ return false;
+ });
+ });
+ return result;
+ };
+
+ // Sends a response to a remote debugging method call (i.e.,
+ // "return") to InspectorTracingControllerClient.
+ this.respondToMethod = function(id, params) {
+ this.devToolsAPI_.dispatchMessage(JSON.stringify({
+ id,
+ result: params,
+ }));
+ };
+
+ // Sets the object used to send messages back to
+ // InspectorTracingControllerClient.
+ this.setDevToolsAPI = function(api) {
+ this.devToolsAPI_ = api;
+ };
+
+ // Sends a notification to InspectorTracingControllerClient.
+ this.sendNotification = function(method, params) {
+ this.devToolsAPI_.dispatchMessage(JSON.stringify({ method, params }));
+ };
+ })();
+ }
+
+ test('shouldUseLegacyTraceFormatIfNoStreamId', async function() {
+ const fakeDevToolsHost = makeFakeDevToolsHost();
+ const fakeWindow = {
+ DevToolsHost: fakeDevToolsHost,
+ };
+ const controller =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(fakeWindow));
+ fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI);
+
+ const runHost = (async() => {
+ const startParams = await fakeDevToolsHost.handleMethod('Tracing.start');
+ fakeDevToolsHost.respondToMethod(startParams.id, {});
+ const endParams = await fakeDevToolsHost.handleMethod('Tracing.end');
+ fakeDevToolsHost.respondToMethod(endParams.id, {});
+ fakeDevToolsHost.sendNotification('Tracing.tracingComplete', {});
+ })();
+
+ await controller.beginRecording({});
+ const traceData = await controller.endRecording();
+ await runHost;
+
+ assert.strictEqual('[]', traceData);
+ });
+
+ test('shouldReassembleTextDataChunks', async function() {
+ const fakeDevToolsHost = makeFakeDevToolsHost();
+ const fakeWindow = {
+ DevToolsHost: fakeDevToolsHost,
+ };
+ const controller =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(fakeWindow));
+ fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI);
+
+ const STREAM_HANDLE = 7;
+
+ const streamChunks = [
+ '[',
+ ']',
+ '\n',
+ ];
+
+ let streamClosed = false;
+
+ const handleIoRead = (index, params) => {
+ if (params.params.handle !== STREAM_HANDLE) {
+ throw new Error('Invalid stream handle');
+ }
+ if (streamClosed) {
+ throw new Error('stream is closed');
+ }
+ let data = '';
+ if (index < streamChunks.length) {
+ data = streamChunks[index];
+ }
+ const eof = (index >= streamChunks.length - 1);
+ fakeDevToolsHost.respondToMethod(params.id, {
+ eof,
+ base64Encoded: false,
+ data,
+ });
+ const nextIndex = eof ? streamChunks.length : index + 1;
+ return (async() =>
+ handleIoRead(nextIndex, await fakeDevToolsHost.handleMethod('IO.read'))
+ )();
+ };
+
+ const runHost = (async() => {
+ const startParams = await fakeDevToolsHost.handleMethod('Tracing.start');
+ fakeDevToolsHost.respondToMethod(startParams.id, {});
+ const endParams = await fakeDevToolsHost.handleMethod('Tracing.end');
+ fakeDevToolsHost.respondToMethod(endParams.id, {});
+ fakeDevToolsHost.sendNotification('Tracing.tracingComplete', {
+ 'stream': STREAM_HANDLE,
+ });
+
+ const closePromise = (async() => {
+ const closeParams = await fakeDevToolsHost.handleMethod('IO.close');
+ assert.strictEqual(closeParams.params.handle, STREAM_HANDLE);
+ streamClosed = true;
+ })();
+
+ const readPromise = (async() =>
+ handleIoRead(0, await fakeDevToolsHost.handleMethod('IO.read'))
+ )();
+
+ await Promise.race([closePromise, readPromise]);
+ await closePromise;
+ })();
+
+ await controller.beginRecording({});
+ const traceData = await controller.endRecording();
+ await runHost;
+
+ assert.strictEqual(traceData, '[]\n');
+ });
+
+ test('shouldReassembleBase64TraceDataChunks', async function() {
+ const fakeDevToolsHost = makeFakeDevToolsHost();
+ const fakeWindow = {
+ DevToolsHost: fakeDevToolsHost,
+ };
+ const controller =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(fakeWindow));
+ fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI);
+
+ const STREAM_HANDLE = 7;
+
+ // This is the empty trace ('[]') gzip compressed and chunked to make
+ // sure reassembling base64 strings works properly.
+ const streamChunks = [
+ 'Hw==',
+ 'iwg=',
+ 'ALg4',
+ 'L1oAA4uOBQApu0wNAgAAAA==',
+ ];
+
+ let streamClosed = false;
+
+ const handleIoRead = (index, params) => {
+ if (params.params.handle !== STREAM_HANDLE) {
+ throw new Error('Invalid stream handle');
+ }
+ if (streamClosed) {
+ throw new Error('stream is closed');
+ }
+ let data = '';
+ if (index < streamChunks.length) {
+ data = streamChunks[index];
+ }
+ const eof = (index >= streamChunks.length - 1);
+ fakeDevToolsHost.respondToMethod(params.id, {
+ eof,
+ base64Encoded: true,
+ data,
+ });
+ const nextIndex = eof ? streamChunks.length : index + 1;
+ return (async() => {
+ handleIoRead(nextIndex, await fakeDevToolsHost.handleMethod('IO.read'));
+ })();
+ };
+
+ const runHost = (async() => {
+ const startParams = await fakeDevToolsHost.handleMethod('Tracing.start');
+ fakeDevToolsHost.respondToMethod(startParams.id, {});
+ const endParams = await fakeDevToolsHost.handleMethod('Tracing.end');
+ fakeDevToolsHost.respondToMethod(endParams.id, {});
+ fakeDevToolsHost.sendNotification('Tracing.tracingComplete', {
+ 'stream': STREAM_HANDLE,
+ 'streamCompression': 'gzip'
+ });
+ const closePromise = (async() => {
+ const closeParams = await fakeDevToolsHost.handleMethod('IO.close');
+ assert.strictEqual(closeParams.params.handle, STREAM_HANDLE);
+ streamClosed = true;
+ })();
+
+ const readPromise = (async() => {
+ handleIoRead(0, await fakeDevToolsHost.handleMethod('IO.read'));
+ })();
+
+ await Promise.race([closePromise, readPromise]);
+ await closePromise;
+ })();
+
+ await controller.beginRecording({});
+ const traceData = await controller.endRecording();
+ await runHost;
+
+ const dataArray = new Uint8Array(traceData);
+ const expectedArray = new Uint8Array([
+ 0x1f, 0x8b, 0x8, 0x0, 0xb8, 0x38, 0x2f, 0x5a, 0x0, 0x3, 0x8b, 0x8e,
+ 0x5, 0x0, 0x29, 0xbb, 0x4c, 0xd, 0x2, 0x0, 0x0, 0x0]);
+
+ assert.strictEqual(dataArray.length, expectedArray.length);
+
+ for (let i = 0; i < dataArray.length; ++i) {
+ assert.strictEqual(dataArray[i], expectedArray[i]);
+ }
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html
new file mode 100644
index 00000000000..cfefdc05cc7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ function MockTracingControllerClient() {
+ this.requests = [];
+ this.nextRequestIndex = 0;
+ this.allowLooping = false;
+ }
+
+ MockTracingControllerClient.prototype = {
+ __proto__: tr.ui.e.about_tracing.TracingControllerClient.prototype,
+
+ expectRequest(method, generateResponse) {
+ let generateResponseCb;
+ if (typeof generateResponse === 'function') {
+ generateResponseCb = generateResponse;
+ } else {
+ generateResponseCb = function() {
+ return generateResponse;
+ };
+ }
+
+ this.requests.push({
+ method,
+ generateResponseCb});
+ },
+
+ _request(method, args) {
+ return new Promise(function(resolve) {
+ const requestIndex = this.nextRequestIndex;
+ if (requestIndex >= this.requests.length) {
+ throw new Error('Unhandled request');
+ }
+ if (!this.allowLooping) {
+ this.nextRequestIndex++;
+ } else {
+ this.nextRequestIndex = (this.nextRequestIndex + 1) %
+ this.requests.length;
+ }
+
+ const req = this.requests[requestIndex];
+ assert.strictEqual(method, req.method);
+ const resp = req.generateResponseCb(args);
+ resolve(resp);
+ }.bind(this));
+ },
+
+ assertAllRequestsHandled() {
+ if (this.allowLooping) {
+ throw new Error('Incompatible with allowLooping');
+ }
+ assert.strictEqual(this.requests.length, this.nextRequestIndex);
+ },
+
+ getCategories() {
+ return this._request('getCategories');
+ },
+
+ beginRecording(recordingOptions) {
+ return this._request('beginRecording', recordingOptions);
+ },
+
+ beginGetBufferPercentFull() {
+ return this._request('beginGetBufferPercentFull');
+ },
+
+ endRecording() {
+ return this._request('endRecording');
+ }
+ };
+
+ return {
+ MockTracingControllerClient,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html
new file mode 100644
index 00000000000..77c0e80af12
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html
@@ -0,0 +1,372 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/ui/base/file.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/info_bar_group.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/record_controller.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<style>
+x-profiling-view {
+ flex-direction: column;
+ display: flex;
+ padding: 0;
+}
+
+x-profiling-view .controls #save-button {
+ margin-left: 64px !important;
+}
+
+x-profiling-view > tr-ui-timeline-view {
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
+.report-id-message {
+ -webkit-user-select: text;
+}
+
+x-timeline-view-buttons {
+ display: flex;
+ align-items: center;
+}
+</style>
+
+<template id="profiling-view-template">
+ <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group>
+ <x-timeline-view-buttons>
+ <button id="record-button">Record</button>
+ <button id="save-button">Save</button>
+ <button id="load-button">Load</button>
+ </x-timeline-view-buttons>
+ <tr-ui-timeline-view>
+ <track-view-container id='track_view_container'></track-view-container>
+ </tr-ui-timeline-view>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview ProfilingView glues the View control to
+ * TracingController.
+ */
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ /**
+ * ProfilingView
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ const ProfilingView = tr.ui.b.define('x-profiling-view');
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ ProfilingView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate(tracingControllerClient) {
+ Polymer.dom(this).appendChild(
+ tr.ui.b.instantiateTemplate('#profiling-view-template', THIS_DOC));
+
+ this.timelineView_ =
+ Polymer.dom(this).querySelector('tr-ui-timeline-view');
+ this.infoBarGroup_ =
+ Polymer.dom(this).querySelector('tr-ui-b-info-bar-group');
+
+ // Detach the buttons. We will reattach them to the timeline view.
+ // TODO(nduca): Make timeline-view have a content select="x-buttons"
+ // that pulls in any buttons.
+ this.recordButton_ = Polymer.dom(this).querySelector('#record-button');
+ this.loadButton_ = Polymer.dom(this).querySelector('#load-button');
+ this.saveButton_ = Polymer.dom(this).querySelector('#save-button');
+
+ const buttons = Polymer.dom(this).querySelector(
+ 'x-timeline-view-buttons');
+ Polymer.dom(buttons.parentElement).removeChild(buttons);
+ Polymer.dom(this.timelineView_.leftControls).appendChild(buttons);
+ this.initButtons_();
+
+ this.timelineView_.hotkeyController.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: 'r'.charCodeAt(0),
+ callback(e) {
+ this.beginRecording();
+ event.stopPropagation();
+ },
+ thisArg: this
+ }));
+
+ this.initDragAndDrop_();
+
+ if (tracingControllerClient) {
+ this.tracingControllerClient_ = tracingControllerClient;
+ } else if (window.DevToolsHost !== undefined) {
+ this.tracingControllerClient_ =
+ new tr.ui.e.about_tracing.InspectorTracingControllerClient(
+ new tr.ui.e.about_tracing.InspectorConnection(window));
+ } else {
+ this.tracingControllerClient_ =
+ new tr.ui.e.about_tracing.XhrBasedTracingControllerClient();
+ }
+
+ this.isRecording_ = false;
+ this.activeTrace_ = undefined;
+
+ this.updateTracingControllerSpecificState_();
+ },
+
+ // Detach all document event listeners. Without this the tests can get
+ // confused as the element may still be listening when the next test runs.
+ detach_() {
+ this.detachDragAndDrop_();
+ },
+
+ get isRecording() {
+ return this.isRecording_;
+ },
+
+ set tracingControllerClient(tracingControllerClient) {
+ this.tracingControllerClient_ = tracingControllerClient;
+ this.updateTracingControllerSpecificState_();
+ },
+
+ updateTracingControllerSpecificState_() {
+ const isInspector = this.tracingControllerClient_ instanceof
+ tr.ui.e.about_tracing.InspectorTracingControllerClient;
+
+ if (isInspector) {
+ this.infoBarGroup_.addMessage(
+ 'This about:tracing is connected to a remote device...',
+ [{buttonText: 'Wow!', onClick() {}}]);
+ }
+ },
+
+ beginRecording() {
+ if (this.isRecording_) {
+ throw new Error('Already recording');
+ }
+ this.isRecording_ = true;
+ const resultPromise = tr.ui.e.about_tracing.beginRecording(
+ this.tracingControllerClient_);
+ resultPromise.then(
+ function(data) {
+ this.isRecording_ = false;
+ const traceName = tr.ui.e.about_tracing.defaultTraceName(
+ this.tracingControllerClient_);
+ this.setActiveTrace(traceName, data, false);
+ }.bind(this),
+ function(err) {
+ this.isRecording_ = false;
+ if (err instanceof tr.ui.e.about_tracing.UserCancelledError) {
+ return;
+ }
+ tr.ui.b.Overlay.showError('Error while recording', err);
+ }.bind(this));
+ return resultPromise;
+ },
+
+ get timelineView() {
+ return this.timelineView_;
+ },
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ clearActiveTrace() {
+ this.saveButton_.disabled = true;
+ this.activeTrace_ = undefined;
+ },
+
+ setActiveTrace(filename, data) {
+ this.activeTrace_ = {
+ filename,
+ data
+ };
+
+ this.infoBarGroup_.clearMessages();
+ this.updateTracingControllerSpecificState_();
+ this.saveButton_.disabled = false;
+ this.timelineView_.viewTitle = filename;
+
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m);
+ const p = i.importTracesWithProgressDialog([data]);
+ p.then(
+ function() {
+ this.timelineView_.model = m;
+ this.timelineView_.updateDocumentFavicon();
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('While importing: ', err);
+ }.bind(this));
+ },
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ initButtons_() {
+ this.recordButton_.addEventListener(
+ 'click', function(event) {
+ event.stopPropagation();
+ this.beginRecording();
+ }.bind(this));
+
+ this.loadButton_.addEventListener(
+ 'click', function(event) {
+ event.stopPropagation();
+ this.onLoadClicked_();
+ }.bind(this));
+
+ this.saveButton_.addEventListener('click',
+ this.onSaveClicked_.bind(this));
+ this.saveButton_.disabled = true;
+ },
+
+ requestFilename_() {
+ // unsafe filename patterns:
+ const illegalRe = /[\/\?<>\\:\*\|":]/g;
+ const controlRe = /[\x00-\x1f\x80-\x9f]/g;
+ const reservedRe = /^\.+$/;
+
+ const defaultName = this.activeTrace_.filename;
+ let fileExtension = '.json';
+ let fileRegex = /\.json$/;
+ if (/[.]gz$/.test(defaultName)) {
+ fileExtension += '.gz';
+ fileRegex = /\.json\.gz$/;
+ } else if (/[.]zip$/.test(defaultName)) {
+ fileExtension = '.zip';
+ fileRegex = /\.zip$/;
+ }
+
+ const custom = prompt('Filename? (' + fileExtension +
+ ' appended) Or leave blank:');
+ if (custom === null) {
+ return undefined;
+ }
+
+ let name;
+ if (custom) {
+ name = ' ' + custom;
+ } else {
+ const date = new Date();
+ const dateText = ' ' + date.toDateString() +
+ ' ' + date.toLocaleTimeString();
+ name = dateText;
+ }
+
+ const filename = defaultName.replace(fileRegex, name) + fileExtension;
+
+ return filename
+ .replace(illegalRe, '.')
+ .replace(controlRe, '\u2022')
+ .replace(reservedRe, '')
+ .replace(/\s+/g, '_');
+ },
+
+ onSaveClicked_() {
+ // Create a blob URL from the binary array.
+ const blob = new Blob([this.activeTrace_.data],
+ {type: 'application/octet-binary'});
+ const blobUrl = window.webkitURL.createObjectURL(blob);
+
+ // Create a link and click on it. BEST API EVAR!
+ const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+ link.href = blobUrl;
+ const filename = this.requestFilename_();
+ if (filename) {
+ link.download = filename;
+ link.click();
+ }
+ },
+
+ onLoadClicked_() {
+ const inputElement = document.createElement('input');
+ inputElement.type = 'file';
+ inputElement.multiple = false;
+
+ let changeFired = false;
+ inputElement.addEventListener(
+ 'change',
+ function(e) {
+ if (changeFired) return;
+ changeFired = true;
+
+ const file = inputElement.files[0];
+ tr.ui.b.readFile(file).then(
+ function(data) {
+ this.setActiveTrace(file.name, data);
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('Error while loading file: ' + err);
+ });
+ }.bind(this), false);
+ inputElement.click();
+ },
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ initDragAndDrop_() {
+ this.dropHandler_ = this.dropHandler_.bind(this);
+ this.ignoreDragEvent_ = this.ignoreDragEvent_.bind(this);
+ document.addEventListener('dragstart', this.ignoreDragEvent_, false);
+ document.addEventListener('dragend', this.ignoreDragEvent_, false);
+ document.addEventListener('dragenter', this.ignoreDragEvent_, false);
+ document.addEventListener('dragleave', this.ignoreDragEvent_, false);
+ document.addEventListener('dragover', this.ignoreDragEvent_, false);
+ document.addEventListener('drop', this.dropHandler_, false);
+ },
+
+ detachDragAndDrop_() {
+ document.removeEventListener('dragstart', this.ignoreDragEvent_);
+ document.removeEventListener('dragend', this.ignoreDragEvent_);
+ document.removeEventListener('dragenter', this.ignoreDragEvent_);
+ document.removeEventListener('dragleave', this.ignoreDragEvent_);
+ document.removeEventListener('dragover', this.ignoreDragEvent_);
+ document.removeEventListener('drop', this.dropHandler_);
+ },
+
+ ignoreDragEvent_(e) {
+ e.preventDefault();
+ return false;
+ },
+
+ dropHandler_(e) {
+ if (this.isAnyDialogUp_) return;
+
+ e.stopPropagation();
+ e.preventDefault();
+
+ const files = e.dataTransfer.files;
+ if (files.length !== 1) {
+ tr.ui.b.Overlay.showError('1 file supported at a time.');
+ return;
+ }
+
+ tr.ui.b.readFile(files[0]).then(
+ function(data) {
+ this.setActiveTrace(files[0].name, data);
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('Error while loading file: ' + err);
+ });
+ return false;
+ }
+ };
+
+ return {
+ ProfilingView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html
new file mode 100644
index 00000000000..f52c491207f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html">
+<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Base64 = tr.b.Base64;
+ const testData = [
+ {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ const monitoringOptions = {
+ isMonitoring: false,
+ categoryFilter: '*',
+ useSystemTracing: false,
+ useContinuousTracing: false,
+ useSampling: false
+ };
+
+ const ProfilingView = tr.ui.e.about_tracing.ProfilingView;
+
+ test('recording', function() {
+ const mock = new tr.ui.e.about_tracing.MockTracingControllerClient();
+ mock.allowLooping = true;
+ mock.expectRequest('endRecording', function() {
+ return '';
+ });
+ mock.expectRequest('getCategories', function() {
+ return ['a', 'b', 'c'];
+ });
+ mock.expectRequest('beginRecording', function(data) {
+ return '';
+ });
+ mock.expectRequest('endRecording', function(data) {
+ return JSON.stringify(testData);
+ });
+
+ const view = new ProfilingView(mock);
+ view.style.height = '400px';
+ view.style.border = '1px solid black';
+ this.addHTMLOutput(view);
+
+ const recordingPromise = view.beginRecording();
+
+ let didAbort = false;
+
+ tr.b.timeout(60).then(() => {
+ if (didAbort) return;
+ recordingPromise.selectionDlg.clickRecordButton();
+ }).then(() => tr.b.timeout(60)).then(() => {
+ recordingPromise.progressDlg.clickStopButton();
+ });
+
+ return recordingPromise.then(null, err => {
+ didAbort = true;
+ assert.fail(err);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html
new file mode 100644
index 00000000000..a9b42b589d8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html
@@ -0,0 +1,187 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/extras/about_tracing/record_selection_dialog.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ function beginRecording(tracingControllerClient) {
+ let finalPromiseResolver;
+ const finalPromise = new Promise(function(resolve, reject) {
+ finalPromiseResolver = {
+ resolve,
+ reject
+ };
+ });
+ finalPromise.selectionDlg = undefined;
+ finalPromise.progressDlg = undefined;
+
+ function beginRecordingError(err) {
+ finalPromiseResolver.reject(err);
+ }
+
+ // Step 0: End recording. This is necessary when the user reloads the
+ // about:tracing page when we are recording. Window.onbeforeunload is not
+ // reliable to end recording on reload.
+ endRecording(tracingControllerClient).then(
+ getCategories,
+ getCategories); // Ignore error.
+
+ // But just in case, bind onbeforeunload anyway.
+ window.onbeforeunload = function(e) {
+ endRecording(tracingControllerClient);
+ };
+
+ // Step 1: Get categories.
+ function getCategories() {
+ const p = tracingControllerClient.getCategories().then(
+ showTracingDialog,
+ beginRecordingError);
+ p.catch(function(err) {
+ beginRecordingError(err);
+ });
+ }
+
+ // Step 2: Show tracing dialog.
+ let selectionDlg;
+ function showTracingDialog(categories) {
+ selectionDlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ selectionDlg.categories = categories;
+ selectionDlg.settings_key =
+ 'tr.ui.e.about_tracing.record_selection_dialog';
+ selectionDlg.addEventListener('recordclick', startTracing);
+ selectionDlg.addEventListener('closeclick', cancelRecording);
+ selectionDlg.visible = true;
+
+ finalPromise.selectionDlg = selectionDlg;
+ }
+
+ function cancelRecording() {
+ finalPromise.selectionDlg = undefined;
+ finalPromiseResolver.reject(new UserCancelledError());
+ }
+
+ // Step 2: Do the actual tracing dialog.
+ let progressDlg;
+ let bufferPercentFullDiv;
+ function startTracing() {
+ progressDlg = new tr.ui.b.Overlay();
+ Polymer.dom(progressDlg).textContent = 'Recording...';
+ progressDlg.userCanClose = false;
+
+ bufferPercentFullDiv = document.createElement('div');
+ Polymer.dom(progressDlg).appendChild(bufferPercentFullDiv);
+
+ const stopButton = document.createElement('button');
+ Polymer.dom(stopButton).textContent = 'Stop';
+ progressDlg.clickStopButton = function() {
+ stopButton.click();
+ };
+ Polymer.dom(progressDlg).appendChild(stopButton);
+
+ const categories = selectionDlg.includedAndExcludedCategories();
+ const recordingOptions = {
+ included_categories: categories.included,
+ excluded_categories: categories.excluded,
+ enable_systrace: selectionDlg.useSystemTracing,
+ record_mode: selectionDlg.tracingRecordMode,
+ };
+ if (categories.included.indexOf(
+ 'disabled-by-default-memory-infra') !== -1) {
+ const memoryConfig = { triggers: [] };
+ memoryConfig.triggers.push(
+ {'mode': 'detailed', 'periodic_interval_ms': 10000});
+ recordingOptions.memory_dump_config = memoryConfig;
+ }
+
+ const requestPromise = tracingControllerClient.beginRecording(
+ recordingOptions);
+ requestPromise.then(
+ function() {
+ progressDlg.visible = true;
+ stopButton.focus();
+ updateBufferPercentFull('0');
+ },
+ recordFailed);
+
+ stopButton.addEventListener('click', function() {
+ // TODO(chrishenry): Currently, this only dismiss the progress
+ // dialog when tracingComplete event is received. When performing
+ // remote debugging, the tracingComplete event may be delayed
+ // considerable. We should indicate to user that we are waiting
+ // for tracingComplete event instead of being unresponsive. (For
+ // now, I disable the "stop" button, since clicking on the button
+ // again now cause exception.)
+ const recordingPromise = endRecording(tracingControllerClient);
+ recordingPromise.then(
+ recordFinished,
+ recordFailed);
+ stopButton.disabled = true;
+ bufferPercentFullDiv = undefined;
+ });
+ finalPromise.progressDlg = progressDlg;
+ }
+
+ function recordFinished(tracedData) {
+ progressDlg.visible = false;
+ finalPromise.progressDlg = undefined;
+ finalPromiseResolver.resolve(tracedData);
+ }
+
+ function recordFailed(err) {
+ progressDlg.visible = false;
+ finalPromise.progressDlg = undefined;
+ finalPromiseResolver.reject(err);
+ }
+
+ function getBufferPercentFull() {
+ if (!bufferPercentFullDiv) return;
+
+ tracingControllerClient.beginGetBufferPercentFull().then(
+ updateBufferPercentFull);
+ }
+
+ function updateBufferPercentFull(percentFull) {
+ if (!bufferPercentFullDiv) return;
+
+ percentFull = Math.round(100 * parseFloat(percentFull));
+ const newText = 'Buffer usage: ' + percentFull + '%';
+ if (Polymer.dom(bufferPercentFullDiv).textContent !== newText) {
+ Polymer.dom(bufferPercentFullDiv).textContent = newText;
+ }
+
+ window.setTimeout(getBufferPercentFull, 500);
+ }
+
+ // Thats it! We're done.
+ return finalPromise;
+ }
+
+ function endRecording(tracingControllerClient) {
+ return tracingControllerClient.endRecording();
+ }
+
+ function defaultTraceName(tracingControllerClient) {
+ return tracingControllerClient.defaultTraceName();
+ }
+
+ function UserCancelledError() {
+ Error.apply(this, arguments);
+ }
+ UserCancelledError.prototype = {
+ __proto__: Error.prototype
+ };
+
+ return {
+ beginRecording,
+ UserCancelledError,
+ defaultTraceName,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html
new file mode 100644
index 00000000000..e3e0438f3a2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/record_controller.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const testData = [
+ {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ test('fullRecording', function() {
+ const mock = new tr.ui.e.about_tracing.MockTracingControllerClient();
+ mock.expectRequest('endRecording', function() {
+ return '';
+ });
+ mock.expectRequest('getCategories', function() {
+ tr.b.timeout(20).then(() =>
+ recordingPromise.selectionDlg.clickRecordButton());
+ return ['a', 'b', 'c'];
+ });
+ mock.expectRequest('beginRecording', function(recordingOptions) {
+ assert.typeOf(recordingOptions.included_categories, 'array');
+ assert.typeOf(recordingOptions.excluded_categories, 'array');
+ assert.typeOf(recordingOptions.enable_systrace, 'boolean');
+ assert.typeOf(recordingOptions.record_mode, 'string');
+ tr.b.timeout(10).then(() =>
+ recordingPromise.progressDlg.clickStopButton());
+ return '';
+ });
+ mock.expectRequest('endRecording', function(data) {
+ return JSON.stringify(testData);
+ });
+
+ const recordingPromise = tr.ui.e.about_tracing.beginRecording(mock);
+
+ return recordingPromise.then(function(data) {
+ mock.assertAllRequestsHandled();
+ assert.strictEqual(data, JSON.stringify(testData));
+ }, function(error) {
+ assert.fail(error);
+ });
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html
new file mode 100644
index 00000000000..a5383973a80
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html
@@ -0,0 +1,689 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/info_bar_group.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<template id="record-selection-dialog-template">
+ <style>
+ .categories-column-view {
+ display: flex;
+ flex-direction: column;
+ font-family: sans-serif;
+ max-width: 640px;
+ min-height: 0;
+ min-width: 0;
+ opacity: 1;
+ transition: max-height 1s ease, max-width 1s ease, opacity 1s ease;
+ will-change: opacity;
+ }
+
+ .categories-column-view-hidden {
+ max-height: 0;
+ max-width: 0;
+ opacity: 0;
+ overflow: hidden;
+ display: none;
+ }
+
+ .categories-selection {
+ display: flex;
+ flex-direction: row;
+ }
+
+ .category-presets {
+ padding: 4px;
+ }
+
+ .category-description {
+ color: #aaa;
+ font-size: small;
+ max-height: 1em;
+ opacity: 1;
+ padding-left: 4px;
+ padding-right: 4px;
+ text-align: right;
+ transition: max-height 1s ease, opacity 1s ease;
+ will-change: opacity;
+ }
+
+ .category-description-hidden {
+ max-height: 0;
+ opacity: 0;
+ }
+
+ .default-enabled-categories,
+ .default-disabled-categories {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ padding: 4px;
+ width: 300px;
+ }
+
+ .default-enabled-categories > div,
+ .default-disabled-categories > div {
+ padding: 4px;
+ }
+
+ .tracing-modes {
+ flex: 1 0 auto;
+ display: flex;
+ flex-direction: reverse;
+ padding: 4px;
+ border-bottom: 2px solid #ddd;
+ border-top: 2px solid #ddd;
+ }
+
+ .default-disabled-categories {
+ border-left: 2px solid #ddd;
+ }
+
+ .warning-default-disabled-categories {
+ display: inline-block;
+ font-weight: bold;
+ text-align: center;
+ color: #BD2E2E;
+ width: 2.0ex;
+ height: 2.0ex;
+ border-radius: 2.0ex;
+ border: 1px solid #BD2E2E;
+ }
+
+ .categories {
+ font-size: 80%;
+ padding: 10px;
+ flex: 1 1 auto;
+ }
+
+ .group-selectors {
+ font-size: 80%;
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 6px;
+ flex: 0 0 auto;
+ }
+
+ .group-selectors button {
+ padding: 1px;
+ }
+
+ .record-selection-dialog .labeled-option-group {
+ flex: 0 0 auto;
+ flex-direction: column;
+ display: flex;
+ }
+
+ .record-selection-dialog .labeled-option {
+ border-top: 5px solid white;
+ border-bottom: 5px solid white;
+ }
+
+ .record-selection-dialog .edit-categories {
+ padding-left: 6px;
+ }
+
+ .record-selection-dialog .edit-categories:after {
+ padding-left: 15px;
+ font-size: 125%;
+ }
+
+ .record-selection-dialog .labeled-option-group:not(.categories-expanded)
+ .edit-categories:after {
+ content: '\25B8'; /* Right triangle */
+ }
+
+ .record-selection-dialog .labeled-option-group.categories-expanded
+ .edit-categories:after {
+ content: '\25BE'; /* Down triangle */
+ }
+
+ </style>
+
+ <div class="record-selection-dialog">
+ <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group>
+ <div class="category-presets">
+ </div>
+ <div class="category-description"></div>
+ <div class="categories-column-view">
+ <div class="tracing-modes"></div>
+ <div class="categories-selection">
+ <div class="default-enabled-categories">
+ <div>Record&nbsp;Categories</div>
+ <div class="group-selectors">
+ Select
+ <button class="all-btn">All</button>
+ <button class="none-btn">None</button>
+ </div>
+ <div class="categories"></div>
+ </div>
+ <div class="default-disabled-categories">
+ <div>Disabled&nbsp;by&nbsp;Default&nbsp;Categories
+ <a class="warning-default-disabled-categories">!</a>
+ </div>
+ <div class="group-selectors">
+ Select
+ <button class="all-btn">All</button>
+ <button class="none-btn">None</button>
+ </div>
+ <div class="categories"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview RecordSelectionDialog presents the available categories
+ * to be enabled/disabled during tr.c.
+ */
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+ const RecordSelectionDialog = tr.ui.b.define('div');
+
+ const DEFAULT_PRESETS = [
+ {title: 'Web developer',
+ categoryFilter: ['blink', 'cc', 'netlog', 'renderer.scheduler',
+ 'sequence_manager', 'toplevel', 'v8']},
+ {title: 'Input latency',
+ categoryFilter: ['benchmark', 'input', 'evdev', 'renderer.scheduler',
+ 'sequence_manager', 'toplevel']},
+ {title: 'Rendering',
+ categoryFilter: ['blink', 'cc', 'gpu', 'toplevel', 'viz']},
+ {title: 'Javascript and rendering',
+ categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler',
+ 'sequence_manager', 'v8', 'toplevel', 'viz']},
+ {title: 'Frame Viewer',
+ categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler',
+ 'sequence_manager', 'v8', 'toplevel',
+ 'disabled-by-default-blink.invalidation',
+ 'disabled-by-default-cc.debug',
+ 'disabled-by-default-cc.debug.picture',
+ 'disabled-by-default-cc.debug.display_items']},
+ {title: 'Manually select settings',
+ categoryFilter: []}
+ ];
+ const RECORDING_MODES = [
+ {'label': 'Record until full',
+ 'value': 'record-until-full'},
+ {'label': 'Record continuously',
+ 'value': 'record-continuously'},
+ {'label': 'Record as much as possible',
+ 'value': 'record-as-much-as-possible'}];
+ const DEFAULT_RECORD_MODE = 'record-until-full';
+ const DEFAULT_CONTINUOUS_TRACING = true;
+ const DEFAULT_SYSTEM_TRACING = true;
+ const DEFAULT_SAMPLING_TRACING = false;
+
+ RecordSelectionDialog.prototype = {
+ __proto__: tr.ui.b.Overlay.prototype,
+
+ decorate() {
+ tr.ui.b.Overlay.prototype.decorate.call(this);
+ this.title = 'Record a new trace...';
+
+ Polymer.dom(this).classList.add('record-dialog-overlay');
+
+ const node =
+ tr.ui.b.instantiateTemplate('#record-selection-dialog-template',
+ THIS_DOC);
+ Polymer.dom(this).appendChild(node);
+
+ this.recordButtonEl_ = document.createElement('button');
+ Polymer.dom(this.recordButtonEl_).textContent = 'Record';
+ this.recordButtonEl_.addEventListener(
+ 'click',
+ this.onRecordButtonClicked_.bind(this));
+ this.recordButtonEl_.style.fontSize = '110%';
+ Polymer.dom(this.buttons).appendChild(this.recordButtonEl_);
+
+ this.categoriesView_ = Polymer.dom(this).querySelector(
+ '.categories-column-view');
+ this.presetsEl_ = Polymer.dom(this).querySelector('.category-presets');
+ Polymer.dom(this.presetsEl_).appendChild(tr.ui.b.createOptionGroup(
+ this, 'currentlyChosenPreset',
+ 'about_tracing.record_selection_dialog_preset',
+ DEFAULT_PRESETS[0].categoryFilter,
+ DEFAULT_PRESETS.map(function(p) {
+ return { label: p.title, value: p.categoryFilter };
+ })));
+
+ this.tracingRecordModeSltr_ = tr.ui.b.createSelector(
+ this, 'tracingRecordMode',
+ 'recordSelectionDialog.tracingRecordMode',
+ DEFAULT_RECORD_MODE, RECORDING_MODES);
+
+ this.systemTracingBn_ = tr.ui.b.createCheckBox(
+ undefined, undefined,
+ 'recordSelectionDialog.useSystemTracing', DEFAULT_SYSTEM_TRACING,
+ 'System tracing');
+ this.samplingTracingBn_ = tr.ui.b.createCheckBox(
+ undefined, undefined,
+ 'recordSelectionDialog.useSampling', DEFAULT_SAMPLING_TRACING,
+ 'State sampling');
+ this.tracingModesContainerEl_ = Polymer.dom(this).querySelector(
+ '.tracing-modes');
+ Polymer.dom(this.tracingModesContainerEl_).appendChild(
+ this.tracingRecordModeSltr_);
+ Polymer.dom(this.tracingModesContainerEl_).appendChild(
+ this.systemTracingBn_);
+ Polymer.dom(this.tracingModesContainerEl_).appendChild(
+ this.samplingTracingBn_);
+
+ this.enabledCategoriesContainerEl_ =
+ Polymer.dom(this).querySelector(
+ '.default-enabled-categories .categories');
+
+ this.disabledCategoriesContainerEl_ =
+ Polymer.dom(this).querySelector(
+ '.default-disabled-categories .categories');
+
+ this.createGroupSelectButtons_(
+ Polymer.dom(this).querySelector('.default-enabled-categories'));
+ this.createGroupSelectButtons_(
+ Polymer.dom(this).querySelector('.default-disabled-categories'));
+ this.createDefaultDisabledWarningDialog_(
+ Polymer.dom(this).querySelector(
+ '.warning-default-disabled-categories'));
+ this.editCategoriesOpened_ = false;
+
+ // TODO(chrishenry): When used with tr.ui.b.Overlay (such as in
+ // chrome://tracing, this does not yet look quite right due to
+ // the 10px overlay content padding (but it's good enough).
+ this.infoBarGroup_ = Polymer.dom(this).querySelector(
+ 'tr-ui-b-info-bar-group');
+
+ this.addEventListener('visible-change', this.onVisibleChange_.bind(this));
+ },
+
+ set supportsSystemTracing(s) {
+ if (s) {
+ this.systemTracingBn_.style.display = undefined;
+ } else {
+ this.systemTracingBn_.style.display = 'none';
+ this.useSystemTracing = false;
+ }
+ },
+
+ get tracingRecordMode() {
+ return this.tracingRecordModeSltr_.selectedValue;
+ },
+ set tracingRecordMode(value) {
+ this.tracingRecordMode_ = value;
+ },
+
+ get useSystemTracing() {
+ return this.systemTracingBn_.checked;
+ },
+ set useSystemTracing(value) {
+ this.systemTracingBn_.checked = !!value;
+ },
+
+ get useSampling() {
+ return this.samplingTracingBn_.checked;
+ },
+ set useSampling(value) {
+ this.samplingTracingBn_.checked = !!value;
+ },
+
+ set categories(c) {
+ if (!(c instanceof Array)) {
+ throw new Error('categories must be an array');
+ }
+ this.categories_ = c;
+
+ for (let i = 0; i < this.categories_.length; i++) {
+ const split = this.categories_[i].split(',');
+ this.categories_[i] = split.shift();
+ if (split.length > 0) {
+ this.categories_ = this.categories_.concat(split);
+ }
+ }
+ },
+
+ set settings_key(k) {
+ this.settings_key_ = k;
+ },
+
+ set settings(s) {
+ throw new Error('Dont use this!');
+ },
+
+ usingPreset_() {
+ return this.currentlyChosenPreset_.length > 0;
+ },
+
+ get currentlyChosenPreset() {
+ return this.currentlyChosenPreset_;
+ },
+
+ set currentlyChosenPreset(preset) {
+ if (!(preset instanceof Array)) {
+ throw new Error('RecordSelectionDialog.currentlyChosenPreset:' +
+ ' preset must be an array.');
+ }
+ this.currentlyChosenPreset_ = preset;
+
+ if (this.usingPreset_()) {
+ this.changeEditCategoriesState_(false);
+ } else {
+ this.updateCategoryColumnView_(true);
+ this.changeEditCategoriesState_(true);
+ }
+ this.updateManualSelectionView_();
+ this.updatePresetDescription_();
+ },
+
+ updateManualSelectionView_() {
+ const classList = Polymer.dom(this.categoriesView_).classList;
+ if (!this.usingPreset_()) {
+ classList.remove('categories-column-view-hidden');
+ } else {
+ if (this.editCategoriesOpened_) {
+ classList.remove('categories-column-view-hidden');
+ } else {
+ classList.add('categories-column-view-hidden');
+ }
+ }
+ },
+
+ updateCategoryColumnView_(shouldReadFromSettings) {
+ const categorySet = Polymer.dom(this).querySelectorAll('.categories');
+ for (let i = 0; i < categorySet.length; ++i) {
+ const categoryGroup = categorySet[i].children;
+ for (let j = 0; j < categoryGroup.length; ++j) {
+ const categoryEl = categoryGroup[j].children[0];
+ categoryEl.checked = shouldReadFromSettings ?
+ tr.b.Settings.get(categoryEl.value, false, this.settings_key_) :
+ false;
+ }
+ }
+ },
+
+ onClickEditCategories() {
+ if (!this.usingPreset_()) return;
+
+ if (!this.editCategoriesOpened_) {
+ this.updateCategoryColumnView_(false);
+ for (let i = 0; i < this.currentlyChosenPreset_.length; ++i) {
+ const categoryEl = this.querySelector('#' +
+ this.currentlyChosenPreset_[i]);
+ if (!categoryEl) continue;
+ categoryEl.checked = true;
+ }
+ }
+
+ this.changeEditCategoriesState_(!this.editCategoriesOpened_);
+ this.updateManualSelectionView_();
+ this.recordButtonEl_.focus();
+ },
+
+ changeEditCategoriesState_(editCategoriesState) {
+ const presetOptionsGroup = Polymer.dom(this).querySelector(
+ '.labeled-option-group');
+ if (!presetOptionsGroup) return;
+
+ this.editCategoriesOpened_ = editCategoriesState;
+ if (this.editCategoriesOpened_) {
+ Polymer.dom(presetOptionsGroup).classList.add('categories-expanded');
+ } else {
+ Polymer.dom(presetOptionsGroup).classList.remove(
+ 'categories-expanded');
+ }
+ },
+
+ updatePresetDescription_() {
+ const description = Polymer.dom(this).querySelector(
+ '.category-description');
+ if (this.usingPreset_()) {
+ description.innerText = this.currentlyChosenPreset_;
+ Polymer.dom(description).classList.remove(
+ 'category-description-hidden');
+ } else {
+ description.innerText = '';
+ if (!Polymer.dom(description).classList.contains(
+ 'category-description-hidden')) {
+ Polymer.dom(description).classList.add('category-description-hidden');
+ }
+ }
+ },
+
+ includedAndExcludedCategories() {
+ let includedCategories = [];
+ let excludedCategories = [];
+ if (this.usingPreset_()) {
+ const allCategories = this.allCategories_();
+ for (const category in allCategories) {
+ const disabledByDefault =
+ category.indexOf('disabled-by-default-') === 0;
+ if (this.currentlyChosenPreset_.indexOf(category) >= 0) {
+ if (disabledByDefault) {
+ includedCategories.push(category);
+ }
+ } else {
+ if (!disabledByDefault) {
+ excludedCategories.push(category);
+ }
+ }
+ }
+ return {
+ included: includedCategories,
+ excluded: excludedCategories
+ };
+ }
+
+ excludedCategories = this.unselectedCategories_();
+ includedCategories = this.enabledDisabledByDefaultCategories_();
+ return {
+ included: includedCategories,
+ excluded: excludedCategories
+ };
+ },
+
+ clickRecordButton() {
+ this.recordButtonEl_.click();
+ },
+
+ onRecordButtonClicked_() {
+ this.visible = false;
+ tr.b.dispatchSimpleEvent(this, 'recordclick');
+ return false;
+ },
+
+ collectInputs_(inputs, isChecked) {
+ const inputsLength = inputs.length;
+ const categories = [];
+ for (let i = 0; i < inputsLength; ++i) {
+ const input = inputs[i];
+ if (input.checked === isChecked) {
+ categories.push(input.value);
+ }
+ }
+ return categories;
+ },
+
+ unselectedCategories_() {
+ const inputs =
+ Polymer.dom(this.enabledCategoriesContainerEl_).querySelectorAll(
+ 'input');
+ return this.collectInputs_(inputs, false);
+ },
+
+ enabledDisabledByDefaultCategories_() {
+ const inputs =
+ Polymer.dom(this.disabledCategoriesContainerEl_).querySelectorAll(
+ 'input');
+ return this.collectInputs_(inputs, true);
+ },
+
+ onVisibleChange_() {
+ if (this.visible) {
+ this.updateForm_();
+ }
+ },
+
+ buildInputs_(inputs, checkedDefault, parent) {
+ const inputsLength = inputs.length;
+ for (let i = 0; i < inputsLength; i++) {
+ const category = inputs[i];
+
+ const inputEl = document.createElement('input');
+ inputEl.type = 'checkbox';
+ inputEl.id = category;
+ inputEl.value = category;
+
+ inputEl.checked = tr.b.Settings.get(
+ category, checkedDefault, this.settings_key_);
+ inputEl.onclick = this.updateSetting_.bind(this);
+
+ const labelEl = document.createElement('label');
+ Polymer.dom(labelEl).textContent =
+ category.replace('disabled-by-default-', '');
+ Polymer.dom(labelEl).setAttribute('for', category);
+
+ const divEl = document.createElement('div');
+ Polymer.dom(divEl).appendChild(inputEl);
+ Polymer.dom(divEl).appendChild(labelEl);
+
+ Polymer.dom(parent).appendChild(divEl);
+ }
+ },
+
+ allCategories_() {
+ // Dedup the categories. We may have things in settings that are also
+ // returned when we query the category list.
+ const categorySet = {};
+ const allCategories =
+ this.categories_.concat(tr.b.Settings.keys(this.settings_key_));
+ const allCategoriesLength = allCategories.length;
+ for (let i = 0; i < allCategoriesLength; ++i) {
+ categorySet[allCategories[i]] = true;
+ }
+ return categorySet;
+ },
+
+ updateForm_() {
+ function ignoreCaseCompare(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ }
+
+ // Clear old categories
+ Polymer.dom(this.enabledCategoriesContainerEl_).innerHTML = '';
+ Polymer.dom(this.disabledCategoriesContainerEl_).innerHTML = '';
+
+ this.recordButtonEl_.focus();
+
+ const allCategories = this.allCategories_();
+ let categories = [];
+ let disabledCategories = [];
+ for (const category in allCategories) {
+ if (category.indexOf('disabled-by-default-') === 0) {
+ disabledCategories.push(category);
+ } else {
+ categories.push(category);
+ }
+ }
+ disabledCategories = disabledCategories.sort(ignoreCaseCompare);
+ categories = categories.sort(ignoreCaseCompare);
+
+ if (this.categories_.length === 0) {
+ this.infoBarGroup_.addMessage(
+ 'No categories found; recording will use default categories.');
+ }
+
+ this.buildInputs_(categories, true, this.enabledCategoriesContainerEl_);
+
+ if (disabledCategories.length > 0) {
+ this.disabledCategoriesContainerEl_.hidden = false;
+ this.buildInputs_(disabledCategories, false,
+ this.disabledCategoriesContainerEl_);
+ }
+ },
+
+ updateSetting_(e) {
+ const checkbox = e.target;
+ tr.b.Settings.set(checkbox.value, checkbox.checked, this.settings_key_);
+
+ // Change the current record mode to 'Manually select settings' from
+ // preset mode if and only if currently user is in preset record mode
+ // and user selects/deselects any category in 'Edit Categories' mode.
+ if (this.usingPreset_()) {
+ this.currentlyChosenPreset_ = []; /* manually select settings */
+ const categoryEl = this.querySelector(
+ '#category-preset-Manually-select-settings');
+ categoryEl.checked = true;
+ const description = Polymer.dom(this).querySelector(
+ '.category-description');
+ description.innerText = '';
+ Polymer.dom(description).classList.add('category-description-hidden');
+ }
+ },
+
+ createGroupSelectButtons_(parent) {
+ const flipInputs = function(dir) {
+ const inputs = Polymer.dom(parent).querySelectorAll('input');
+ for (let i = 0; i < inputs.length; i++) {
+ if (inputs[i].checked === dir) continue;
+ // click() is used so the settings will be correclty stored. Setting
+ // checked does not trigger the onclick (or onchange) callback.
+ inputs[i].click();
+ }
+ };
+
+ const allBtn = Polymer.dom(parent).querySelector('.all-btn');
+ allBtn.onclick = function(evt) {
+ flipInputs(true);
+ evt.preventDefault();
+ };
+
+ const noneBtn = Polymer.dom(parent).querySelector('.none-btn');
+ noneBtn.onclick = function(evt) {
+ flipInputs(false);
+ evt.preventDefault();
+ };
+ },
+
+ setWarningDialogOverlayText_(messages) {
+ const contentDiv = document.createElement('div');
+
+ for (let i = 0; i < messages.length; ++i) {
+ const messageDiv = document.createElement('div');
+ Polymer.dom(messageDiv).textContent = messages[i];
+ Polymer.dom(contentDiv).appendChild(messageDiv);
+ }
+ Polymer.dom(this.warningOverlay_).textContent = '';
+ Polymer.dom(this.warningOverlay_).appendChild(contentDiv);
+ },
+
+ createDefaultDisabledWarningDialog_(warningLink) {
+ function onClickHandler(evt) {
+ this.warningOverlay_ = tr.ui.b.Overlay();
+ this.warningOverlay_.parentEl_ = this;
+ this.warningOverlay_.title = 'Warning...';
+ this.warningOverlay_.userCanClose = true;
+ this.warningOverlay_.visible = true;
+
+ this.setWarningDialogOverlayText_([
+ 'Enabling the default disabled categories may have',
+ 'performance and memory impact while tr.c.'
+ ]);
+
+ evt.preventDefault();
+ }
+ warningLink.onclick = onClickHandler.bind(this);
+ }
+ };
+
+ return {
+ RecordSelectionDialog,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html
new file mode 100644
index 00000000000..7c62b487305
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html
@@ -0,0 +1,426 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/extras/about_tracing/record_selection_dialog.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantitate', function() {
+ const showButton = document.createElement('button');
+ Polymer.dom(showButton).textContent = 'Show record selection dialog';
+ this.addHTMLOutput(showButton);
+
+ showButton.addEventListener('click', function(e) {
+ e.stopPropagation();
+
+ const categories = [];
+ for (let i = 0; i < 30; i++) {
+ categories.push('cat-' + i);
+ }
+ for (let i = 0; i < 20; i++) {
+ categories.push('disabled-by-default-cat-' + i);
+ }
+ categories.push(
+ 'really-really-really-really-really-really-very-loong-cat');
+ categories.push('first,second,third');
+ categories.push('cc,disabled-by-default-cc.debug');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = categories;
+ dlg.settings_key = 'key';
+ dlg.visible = true;
+ });
+ });
+
+ test('recordSelectionDialog_splitCategories', function() {
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories =
+ ['cc,disabled-by-default-one,cc.debug', 'two,three', 'three'];
+ dlg.settings_key = 'key';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ const expected =
+ ['"cc"', '"cc.debug"', '"disabled-by-default-one"', '"three"', '"two"'];
+
+ const labels = Polymer.dom(dlg).querySelectorAll('.categories input');
+ let results = [];
+ for (let i = 0; i < labels.length; i++) {
+ results.push('"' + labels[i].value + '"');
+ }
+ results = results.sort();
+
+ assert.deepEqual(results, expected);
+ });
+
+ test('recordSelectionDialog_UpdateForm_NoSettings', function() {
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one', 'two', 'three'];
+ dlg.settings_key = 'key';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input');
+ assert.strictEqual(checkboxes.length, 3);
+ assert.strictEqual(checkboxes[0].id, 'three');
+ assert.strictEqual(checkboxes[0].value, 'three');
+ assert.isTrue(checkboxes[0].checked);
+ assert.strictEqual(checkboxes[1].id, 'two');
+ assert.strictEqual(checkboxes[1].value, 'two');
+ assert.isTrue(checkboxes[1].checked);
+ assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one');
+ assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one');
+ assert.isFalse(checkboxes[2].checked);
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+
+ const labels = Polymer.dom(dlg).querySelectorAll('.categories label');
+ assert.strictEqual(labels.length, 3);
+ assert.strictEqual(Polymer.dom(labels[0]).textContent, 'three');
+ assert.strictEqual(Polymer.dom(labels[1]).textContent, 'two');
+ assert.strictEqual(Polymer.dom(labels[2]).textContent, 'one');
+ });
+
+ test('recordSelectionDialog_UpdateForm_Settings', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input');
+ assert.strictEqual(checkboxes.length, 3);
+ assert.strictEqual(checkboxes[0].id, 'three');
+ assert.strictEqual(checkboxes[0].value, 'three');
+ assert.isFalse(checkboxes[0].checked);
+ assert.strictEqual(checkboxes[1].id, 'two');
+ assert.strictEqual(checkboxes[1].value, 'two');
+ assert.isTrue(checkboxes[1].checked);
+ assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one');
+ assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one');
+ assert.isFalse(checkboxes[2].checked);
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']);
+
+ const labels = Polymer.dom(dlg).querySelectorAll('.categories label');
+ assert.strictEqual(labels.length, 3);
+ assert.strictEqual(Polymer.dom(labels[0]).textContent, 'three');
+ assert.strictEqual(Polymer.dom(labels[1]).textContent, 'two');
+ assert.strictEqual(Polymer.dom(labels[2]).textContent, 'one');
+ });
+
+ test('recordSelectionDialog_UpdateForm_DisabledByDefault', function() {
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-bar', 'baz'];
+ dlg.settings_key = 'categories';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+
+ const inputs =
+ Polymer.dom(dlg).querySelector('input#disabled-by-default-bar').click();
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-bar']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+
+ assert.isFalse(
+ tr.b.Settings.get('disabled-by-default-foo', false, 'categories'));
+ });
+
+ test('selectAll', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+ });
+
+ test('selectNone', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ // Enables the three option, two already enabled.
+ Polymer.dom(dlg).querySelector('.default-enabled-categories .all-btn')
+ .click();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+ assert.isTrue(tr.b.Settings.get('three', false, 'categories'));
+
+ // Disables three and two.
+ Polymer.dom(dlg).querySelector('.default-enabled-categories .none-btn')
+ .click();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+ assert.isFalse(tr.b.Settings.get('two', false, 'categories'));
+ assert.isFalse(tr.b.Settings.get('three', false, 'categories'));
+
+ // Turn categories back on so they can be ignored.
+ Polymer.dom(dlg).querySelector('.default-enabled-categories .all-btn')
+ .click();
+
+ // Enables disabled category.
+ Polymer.dom(dlg).querySelector('.default-disabled-categories .all-btn')
+ .click();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-one']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+ assert.isTrue(
+ tr.b.Settings.get('disabled-by-default-one', false, 'categories'));
+
+ // Turn disabled by default back off.
+ Polymer.dom(dlg).querySelector('.default-disabled-categories .none-btn')
+ .click();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+ assert.isFalse(
+ tr.b.Settings.get('disabled-by-default-one', false, 'categories'));
+ });
+
+ test('recordSelectionDialog_noPreset', function() {
+ tr.b.Settings.set('about_tracing.record_selection_dialog_preset', []);
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ assert.isFalse(dlg.usingPreset_());
+ });
+
+ test('recordSelectionDialog_defaultPreset', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ // Note: currentlyChosenPreset is not set here, so the default is used.
+ dlg.updateForm_();
+
+ // Make sure the default filter is returned.
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+
+ // Make sure the default tracing types are returned.
+ assert.strictEqual(dlg.tracingRecordMode, 'record-until-full');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isFalse(dlg.useSampling);
+
+ // Make sure the manual settings are not visible.
+ const classList = Polymer.dom(dlg.categoriesView_).classList;
+ assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+ // Verify manual settings do not modify the checkboxes.
+ const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input');
+ assert.strictEqual(checkboxes.length, 3);
+ assert.strictEqual(checkboxes[0].id, 'three');
+ assert.strictEqual(checkboxes[0].value, 'three');
+ assert.isFalse(checkboxes[0].checked);
+ assert.strictEqual(checkboxes[1].id, 'two');
+ assert.strictEqual(checkboxes[1].value, 'two');
+ assert.isTrue(checkboxes[1].checked);
+ assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one');
+ assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one');
+ assert.isFalse(checkboxes[2].checked);
+ });
+
+ test('recordSelectionDialog_editPreset', function() {
+ function createDialog() {
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['one', 'two', 'disabled-by-default-three'];
+ dlg.settings_key = 'categories';
+ // Note: currentlyChosenPreset is not set here, so the default is used.
+ dlg.updateForm_();
+ return dlg;
+ }
+
+ // After the dialog is created, it should be using the default preset.
+ let dlg = createDialog();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['one', 'two']);
+ assert.isTrue(dlg.usingPreset_());
+ assert.isFalse(
+ dlg.querySelector('#category-preset-Manually-select-settings').checked);
+
+ // After clicking on "Edit Categories", the default preset should still be
+ // used.
+ dlg.onClickEditCategories();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['one', 'two']);
+ assert.isTrue(dlg.usingPreset_());
+ assert.isFalse(
+ dlg.querySelector('#category-preset-Manually-select-settings').checked);
+
+ // After clicking on category checkbox(es), the mode should be changed to
+ // "Manually select settings".
+ Array.prototype.forEach.call(dlg.querySelectorAll('.categories input'),
+ checkbox => checkbox.click());
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-three']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []);
+ assert.isFalse(dlg.usingPreset_());
+ assert.isTrue(
+ dlg.querySelector('#category-preset-Manually-select-settings').checked);
+
+ // After the dialog is opened again, it should be using the default preset.
+ // More importantly, the default preset should NOT be modified.
+ dlg = createDialog();
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['one', 'two']);
+ assert.isTrue(dlg.usingPreset_());
+ assert.isFalse(
+ dlg.querySelector('#category-preset-Manually-select-settings').checked);
+ });
+
+ test('recordSelectionDialog_changePresets', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+ tr.b.Settings.set('disabled-by-default-cc.debug', true, 'categories');
+ tr.b.Settings.set('recordSelectionDialog.tracingRecordMode',
+ 'record-as-much-as-possible');
+ tr.b.Settings.set('recordSelectionDialog.useSystemTracing', true);
+ tr.b.Settings.set('recordSelectionDialog.useSampling', false);
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ // Note: currentlyChosenPreset is not set here, so the default is used.
+ dlg.updateForm_();
+
+ // Preset mode is on.
+ assert.isTrue(dlg.usingPreset_());
+
+ // Make sure the default filter is returned.
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+
+ // Make sure the default tracing types are returned.
+ assert.strictEqual(dlg.tracingRecordMode, 'record-as-much-as-possible');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isFalse(dlg.useSampling);
+
+ // Make sure the manual settings are not visible.
+ const classList = Polymer.dom(dlg.categoriesView_).classList;
+ assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+ // Switch to manual settings and verify the default values are not returned.
+ dlg.currentlyChosenPreset = [];
+
+ // Preset mode is off.
+ assert.isFalse(dlg.usingPreset_());
+
+ // Make sure the default filter is returned.
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-cc.debug']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']);
+
+ // Make sure the tracing types set by catalog are returned.
+ assert.strictEqual(dlg.tracingRecordMode, 'record-as-much-as-possible');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isFalse(dlg.useSampling);
+ assert.isFalse(classList.contains('categories-column-view-hidden'));
+
+ // Switch to the graphics, rendering, and rasterization preset.
+ dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer',
+ 'disabled-by-default-cc.debug'];
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-cc.debug']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+ });
+
+ test('recordSelectionDialog_savedPreset', function() {
+ tr.b.Settings.set('two', true, 'categories');
+ tr.b.Settings.set('three', false, 'categories');
+ tr.b.Settings.set('recordSelectionDialog.tracingRecordMode',
+ 'record-continuously');
+ tr.b.Settings.set('recordSelectionDialog.useSystemTracing', true);
+ tr.b.Settings.set('recordSelectionDialog.useSampling', true);
+ tr.b.Settings.set('tr.ui.e.about_tracing.record_selection_dialog_preset',
+ ['blink', 'cc', 'renderer', 'cc.debug']);
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.categories = ['disabled-by-default-one'];
+ dlg.settings_key = 'categories';
+ dlg.updateForm_();
+
+ // Make sure the correct filter is returned.
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['three', 'two']);
+
+ // Make sure the correct tracing types are returned.
+ assert.strictEqual(dlg.tracingRecordMode, 'record-continuously');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isTrue(dlg.useSampling);
+
+ // Make sure the manual settings are not visible.
+ const classList = Polymer.dom(dlg.categoriesView_).classList;
+ assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+ // Switch to manual settings and verify the default values are not returned.
+ dlg.currentlyChosenPreset = [];
+ assert.deepEqual(dlg.includedAndExcludedCategories().included, []);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']);
+ assert.strictEqual(dlg.tracingRecordMode, 'record-continuously');
+ assert.isTrue(dlg.useSystemTracing);
+ assert.isTrue(dlg.useSampling);
+ assert.isFalse(classList.contains('categories-column-view-hidden'));
+ });
+
+ test('recordSelectionDialog_categoryFilters', function() {
+ tr.b.Settings.set('default1', true, 'categories');
+ tr.b.Settings.set('disabled1', false, 'categories');
+ tr.b.Settings.set('disabled-by-default-cc.disabled2', false, 'categories');
+ tr.b.Settings.set('input', true, 'categories');
+ tr.b.Settings.set('blink', true, 'categories');
+ tr.b.Settings.set('cc', false, 'categories');
+ tr.b.Settings.set('disabled-by-default-cc.debug', true, 'categories');
+
+ const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog();
+ dlg.settings_key = 'categories';
+ dlg.categories = [];
+ dlg.currentlyChosenPreset = [];
+ dlg.updateForm_();
+
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-cc.debug']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['cc', 'disabled1']);
+
+ // Switch to the graphics, rendering, and rasterization preset.
+ dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer',
+ 'disabled-by-default-cc.debug'];
+ assert.deepEqual(dlg.includedAndExcludedCategories().included,
+ ['disabled-by-default-cc.debug']);
+ assert.deepEqual(dlg.includedAndExcludedCategories().excluded,
+ ['default1', 'disabled1', 'input']);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html
new file mode 100644
index 00000000000..c00bbe915e4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ /**
+ * Communicates with content/browser/tracing_controller_impl.cc
+ *
+ * @constructor
+ */
+ class TracingControllerClient {
+ beginMonitoring(monitoringOptions) { }
+ endMonitoring() { }
+ captureMonitoring() { }
+ getMonitoringStatus() { }
+ getCategories() { }
+ beginRecording(recordingOptions) { }
+ beginGetBufferPercentFull() { }
+ endRecording() { }
+ defaultTraceName() { }
+ }
+
+ return {
+ TracingControllerClient,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html
new file mode 100644
index 00000000000..d2c6adcac2a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import"
+ href="/tracing/ui/extras/about_tracing/tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.about_tracing', function() {
+ const Base64 = tr.b.Base64;
+
+ function beginXhr(method, path, data) {
+ if (data === undefined) data = null;
+
+ return new Promise(function(resolve, reject) {
+ const req = new XMLHttpRequest();
+ if (method !== 'POST' && data !== null) {
+ throw new Error('Non-POST should have data==null');
+ }
+ req.open(method, path, true);
+ req.onreadystatechange = function(e) {
+ if (req.readyState === 4) {
+ window.setTimeout(function() {
+ if (req.status === 200 && req.responseText !== '##ERROR##') {
+ resolve(req.responseText);
+ } else {
+ reject(new Error('Error occured at ' + path));
+ }
+ }, 0);
+ }
+ };
+ req.send(data);
+ });
+ }
+
+ /**
+ * @constructor
+ */
+ function XhrBasedTracingControllerClient() { }
+
+ XhrBasedTracingControllerClient.prototype = {
+ __proto__: tr.ui.e.about_tracing.TracingControllerClient.prototype,
+
+ beginMonitoring(monitoringOptions) {
+ const monitoringOptionsB64 = Base64.btoa(JSON.stringify(
+ monitoringOptions));
+ return beginXhr('GET', '/json/begin_monitoring?' + monitoringOptionsB64);
+ },
+
+ endMonitoring() {
+ return beginXhr('GET', '/json/end_monitoring');
+ },
+
+ captureMonitoring() {
+ return beginXhr('GET', '/json/capture_monitoring_compressed').then(
+ function(data) {
+ const decodedSize = Base64.getDecodedBufferLength(data);
+ const buffer = new ArrayBuffer(decodedSize);
+ Base64.DecodeToTypedArray(data, new DataView(buffer));
+ return buffer;
+ }
+ );
+ },
+
+ getMonitoringStatus() {
+ return beginXhr('GET', '/json/get_monitoring_status').then(
+ function(monitoringOptionsB64) {
+ return JSON.parse(Base64.atob(monitoringOptionsB64));
+ });
+ },
+
+ getCategories() {
+ return beginXhr('GET', '/json/categories').then(
+ function(json) {
+ return JSON.parse(json);
+ });
+ },
+
+ beginRecording(recordingOptions) {
+ const recordingOptionsB64 = Base64.btoa(JSON.stringify(recordingOptions));
+ return beginXhr('GET', '/json/begin_recording?' +
+ recordingOptionsB64);
+ },
+
+ beginGetBufferPercentFull() {
+ return beginXhr('GET', '/json/get_buffer_percent_full');
+ },
+
+ endRecording() {
+ return beginXhr('GET', '/json/end_recording_compressed').then(
+ function(data) {
+ const decodedSize = Base64.getDecodedBufferLength(data);
+ const buffer = new ArrayBuffer(decodedSize);
+ Base64.DecodeToTypedArray(data, new DataView(buffer));
+ return buffer;
+ }
+ );
+ },
+
+ defaultTraceName() {
+ return 'trace.json.gz';
+ }
+ };
+
+ return {
+ XhrBasedTracingControllerClient,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html
new file mode 100644
index 00000000000..79ba7e593c0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_list_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/tile_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html
new file mode 100644
index 00000000000..f8bfd671355
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html
@@ -0,0 +1,451 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_list_item.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html">
+
+<template id="tr-ui-e-chrome-cc-display-item-debugger-template">
+ <left-panel>
+ <display-item-info>
+ <header>
+ <span class='title'>Display Item List</span>
+ <span class='size'></span>
+ <div class='export'>
+ <input class='dlfilename' type='text' value='displayitemlist.json' />
+ <button class='dlexport'>Export display item list</button>
+ </div>
+ <div class='export'>
+ <input class='skpfilename' type='text' value='skpicture.skp' />
+ <button class='skpexport'>Export list as SkPicture</button>
+ </div>
+ </header>
+ </display-item-info>
+ </left-panel>
+ <right-panel>
+ <raster-area>
+ <canvas-scroller>
+ <canvas></canvas>
+ </canvas-scroller>
+ </raster-area>
+ </right-panel>
+</template>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ /**
+ * DisplayItemDebugger is a view of a DisplayItemListSnapshot for inspecting
+ * a display item list and the pictures within it.
+ *
+ * @constructor
+ */
+ const DisplayItemDebugger = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-display-item-debugger');
+
+ DisplayItemDebugger.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ const node = tr.ui.b.instantiateTemplate(
+ '#tr-ui-e-chrome-cc-display-item-debugger-template', THIS_DOC);
+
+ Polymer.dom(this).appendChild(node);
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.display = 'flex';
+ this.style.minWidth = 0;
+
+ this.pictureAsImageData_ = undefined;
+ this.zoomScaleValue_ = 1;
+
+ this.sizeInfo_ = Polymer.dom(this).querySelector('.size');
+ this.rasterArea_ = Polymer.dom(this).querySelector('raster-area');
+ this.rasterArea_.style.flexGrow = 1;
+ this.rasterArea_.style.flexShrink = 1;
+ this.rasterArea_.style.flexBasis = 'auto';
+ this.rasterArea_.style.backgroundColor = '#ddd';
+ this.rasterArea_.style.minHeight = '200px';
+ this.rasterArea_.style.minWidth = '200px';
+ this.rasterArea_.style.paddingLeft = '5px';
+ this.rasterArea_.style.display = 'flex';
+ this.rasterArea_.style.flexDirection = 'column';
+ this.rasterCanvas_ =
+ Polymer.dom(this.rasterArea_).querySelector('canvas');
+ this.rasterCtx_ = this.rasterCanvas_.getContext('2d');
+
+ const canvasScroller = Polymer.dom(this).querySelector('canvas-scroller');
+ canvasScroller.style.flexGrow = 1;
+ canvasScroller.style.flexShrink = 1;
+ canvasScroller.style.flexBasis = 'auto';
+ canvasScroller.style.minWidth = 0;
+ canvasScroller.style.minHeight = 0;
+ canvasScroller.style.overflow = 'auto';
+
+ this.trackMouse_();
+
+ this.displayItemInfo_ =
+ Polymer.dom(this).querySelector('display-item-info');
+ this.displayItemInfo_.addEventListener(
+ 'click', this.onDisplayItemInfoClick_.bind(this), false);
+
+ this.displayItemListView_ = new tr.ui.b.ListView();
+ this.displayItemListView_.addEventListener('selection-changed',
+ this.onDisplayItemListSelection_.bind(this));
+ Polymer.dom(this.displayItemInfo_).appendChild(this.displayItemListView_);
+
+ this.displayListFilename_ =
+ Polymer.dom(this).querySelector('.dlfilename');
+ this.displayListExportButton_ =
+ Polymer.dom(this).querySelector('.dlexport');
+ this.displayListExportButton_.addEventListener(
+ 'click', this.onExportDisplayListClicked_.bind(this));
+
+ this.skpFilename_ = Polymer.dom(this).querySelector('.skpfilename');
+ this.skpExportButton_ = Polymer.dom(this).querySelector('.skpexport');
+ this.skpExportButton_.addEventListener(
+ 'click', this.onExportSkPictureClicked_.bind(this));
+
+ const leftPanel = Polymer.dom(this).querySelector('left-panel');
+ leftPanel.style.flexGrow = 0;
+ leftPanel.style.flexShrink = 0;
+ leftPanel.style.flexBasis = 'auto';
+ leftPanel.style.minWidth = '200px';
+ leftPanel.style.overflow = 'auto';
+
+ leftPanel.children[0].paddingTop = '2px';
+ leftPanel.children[0].children[0].style.borderBottom = '1px solid #555';
+
+ const leftPanelTitle = leftPanel.querySelector('.title');
+ leftPanelTitle.style.fontWeight = 'bold';
+ leftPanelTitle.style.marginLeft = '5px';
+ leftPanelTitle.style.marginright = '5px';
+
+ for (const div of leftPanel.querySelectorAll('.export')) {
+ div.style.margin = '5px';
+ }
+
+ const middleDragHandle = document.createElement('tr-ui-b-drag-handle');
+ middleDragHandle.style.flexGrow = 0;
+ middleDragHandle.style.flexShrink = 0;
+ middleDragHandle.style.flexBasis = 'auto';
+ middleDragHandle.horizontal = false;
+ middleDragHandle.target = leftPanel;
+
+ const rightPanel = Polymer.dom(this).querySelector('right-panel');
+ rightPanel.style.display = 'flex';
+ rightPanel.style.flexGrow = 1;
+ rightPanel.style.flexShrink = 1;
+ rightPanel.style.flexBasis = 'auto';
+ rightPanel.style.minWidth = 0;
+
+ this.infoBar_ = document.createElement('tr-ui-b-info-bar');
+ Polymer.dom(this.rasterArea_).insertBefore(this.infoBar_, canvasScroller);
+
+ Polymer.dom(this).insertBefore(middleDragHandle, rightPanel);
+
+ this.picture_ = undefined;
+
+ this.pictureOpsListView_ = new tr.ui.e.chrome.cc.PictureOpsListView();
+ this.pictureOpsListView_.style.flexGrow = 0;
+ this.pictureOpsListView_.style.flexShrink = 0;
+ this.pictureOpsListView_.style.flexBasis = 'auto';
+ this.pictureOpsListView_.style.overflow = 'auto';
+ this.pictureOpsListView_.style.minWidth = '100px';
+ Polymer.dom(rightPanel).insertBefore(
+ this.pictureOpsListView_, this.rasterArea_);
+
+ this.pictureOpsListDragHandle_ =
+ document.createElement('tr-ui-b-drag-handle');
+ this.pictureOpsListDragHandle_.horizontal = false;
+ this.pictureOpsListDragHandle_.target = this.pictureOpsListView_;
+ Polymer.dom(rightPanel).insertBefore(
+ this.pictureOpsListDragHandle_, this.rasterArea_);
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set displayItemList(displayItemList) {
+ this.displayItemList_ = displayItemList;
+ this.picture = this.displayItemList_;
+
+ this.displayItemListView_.clear();
+ this.displayItemList_.items.forEach(function(item) {
+ const listItem = document.createElement(
+ 'tr-ui-e-chrome-cc-display-item-list-item');
+ listItem.data = item;
+ Polymer.dom(this.displayItemListView_).appendChild(listItem);
+ }.bind(this));
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+
+ // Hide the ops list if we are showing the "main" display item list.
+ const showOpsList = picture && picture !== this.displayItemList_;
+ this.updateDrawOpsList_(showOpsList);
+
+ if (picture) {
+ const size = this.getRasterCanvasSize_();
+ this.rasterCanvas_.width = size.width;
+ this.rasterCanvas_.height = size.height;
+ }
+
+ const bounds = this.rasterArea_.getBoundingClientRect();
+ const selectorBounds = this.mouseModeSelector_.getBoundingClientRect();
+ this.mouseModeSelector_.pos = {
+ x: (bounds.right - selectorBounds.width - 10),
+ y: bounds.top
+ };
+
+ this.rasterize_();
+
+ this.scheduleUpdateContents_();
+ },
+
+ getRasterCanvasSize_() {
+ const style = window.getComputedStyle(this.rasterArea_);
+ let width = parseInt(style.width);
+ let height = parseInt(style.height);
+ if (this.picture_) {
+ width = Math.max(width, this.picture_.layerRect.width);
+ height = Math.max(height, this.picture_.layerRect.height);
+ }
+
+ return {
+ width,
+ height
+ };
+ },
+
+ scheduleUpdateContents_() {
+ if (this.updateContentsPending_) return;
+
+ this.updateContentsPending_ = true;
+ tr.b.requestAnimationFrameInThisFrameIfPossible(
+ this.updateContents_.bind(this)
+ );
+ },
+
+ updateContents_() {
+ this.updateContentsPending_ = false;
+
+ if (this.picture_) {
+ Polymer.dom(this.sizeInfo_).textContent = '(' +
+ this.picture_.layerRect.width + ' x ' +
+ this.picture_.layerRect.height + ')';
+ }
+
+ // Return if picture hasn't finished rasterizing.
+ if (!this.pictureAsImageData_) return;
+
+ this.infoBar_.visible = false;
+ this.infoBar_.removeAllButtons();
+ if (this.pictureAsImageData_.error) {
+ this.infoBar_.message = 'Cannot rasterize...';
+ this.infoBar_.addButton('More info...', function(e) {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = this.pictureAsImageData_.error;
+ overlay.visible = true;
+ e.stopPropagation();
+ return false;
+ }.bind(this));
+ this.infoBar_.visible = true;
+ }
+
+ this.drawPicture_();
+ },
+
+ drawPicture_() {
+ const size = this.getRasterCanvasSize_();
+ if (size.width !== this.rasterCanvas_.width) {
+ this.rasterCanvas_.width = size.width;
+ }
+ if (size.height !== this.rasterCanvas_.height) {
+ this.rasterCanvas_.height = size.height;
+ }
+
+ this.rasterCtx_.clearRect(0, 0, size.width, size.height);
+
+ if (!this.picture_ || !this.pictureAsImageData_.imageData) return;
+
+ const imgCanvas = this.pictureAsImageData_.asCanvas();
+ const w = imgCanvas.width;
+ const h = imgCanvas.height;
+ this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h,
+ 0, 0, w * this.zoomScaleValue_,
+ h * this.zoomScaleValue_);
+ },
+
+ rasterize_() {
+ if (this.picture_) {
+ this.picture_.rasterize(
+ {
+ showOverdraw: false
+ },
+ this.onRasterComplete_.bind(this));
+ }
+ },
+
+ onRasterComplete_(pictureAsImageData) {
+ this.pictureAsImageData_ = pictureAsImageData;
+ this.scheduleUpdateContents_();
+ },
+
+ onDisplayItemListSelection_(e) {
+ const selected = this.displayItemListView_.selectedElement;
+
+ if (!selected) {
+ this.picture = this.displayItemList_;
+ return;
+ }
+
+ const index = Array.prototype.indexOf.call(
+ this.displayItemListView_.children, selected);
+ const displayItem = this.displayItemList_.items[index];
+ if (displayItem && displayItem.skp64) {
+ this.picture = new tr.e.cc.Picture(
+ displayItem.skp64, this.displayItemList_.layerRect);
+ } else {
+ this.picture = undefined;
+ }
+ },
+
+ onDisplayItemInfoClick_(e) {
+ if (e && e.target === this.displayItemInfo_) {
+ this.displayItemListView_.selectedElement = undefined;
+ }
+ },
+
+ updateDrawOpsList_(showOpsList) {
+ if (showOpsList) {
+ this.pictureOpsListView_.picture = this.picture_;
+ if (this.pictureOpsListView_.numOps > 0) {
+ this.pictureOpsListView_.style.display = 'block';
+ this.pictureOpsListDragHandle_.style.display = 'block';
+ }
+ } else {
+ this.pictureOpsListView_.style.display = 'none';
+ this.pictureOpsListDragHandle_.style.display = 'none';
+ }
+ },
+
+ trackMouse_() {
+ this.mouseModeSelector_ = document.createElement(
+ 'tr-ui-b-mouse-mode-selector');
+ this.mouseModeSelector_.targetElement = this.rasterArea_;
+ Polymer.dom(this.rasterArea_).appendChild(this.mouseModeSelector_);
+
+ this.mouseModeSelector_.supportedModeMask =
+ tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.defaultMode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector';
+
+ this.mouseModeSelector_.addEventListener('beginzoom',
+ this.onBeginZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('updatezoom',
+ this.onUpdateZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('endzoom',
+ this.onEndZoom_.bind(this));
+ },
+
+ onBeginZoom_(e) {
+ this.isZooming_ = true;
+
+ this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+
+ e.preventDefault();
+ },
+
+ onUpdateZoom_(e) {
+ if (!this.isZooming_) return;
+
+ const currentMouseViewPos = this.extractRelativeMousePosition_(e);
+
+ // Take the distance the mouse has moved and we want to zoom at about
+ // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned
+ // more if people feel it's too slow.
+ this.zoomScaleValue_ +=
+ ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001);
+ this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1);
+
+ this.drawPicture_();
+
+ this.lastMouseViewPos_ = currentMouseViewPos;
+ },
+
+ onEndZoom_(e) {
+ this.lastMouseViewPos_ = undefined;
+ this.isZooming_ = false;
+ e.preventDefault();
+ },
+
+ extractRelativeMousePosition_(e) {
+ return {
+ x: e.clientX - this.rasterArea_.offsetLeft,
+ y: e.clientY - this.rasterArea_.offsetTop
+ };
+ },
+
+ saveFile_(filename, rawData) {
+ if (!rawData) return;
+
+ // Convert this String into an Uint8Array
+ const length = rawData.length;
+ const arrayBuffer = new ArrayBuffer(length);
+ const uint8Array = new Uint8Array(arrayBuffer);
+ for (let c = 0; c < length; c++) {
+ uint8Array[c] = rawData.charCodeAt(c);
+ }
+
+ // Create a blob URL from the binary array.
+ const blob = new Blob([uint8Array], {type: 'application/octet-binary'});
+ const blobUrl = window.URL.createObjectURL(blob);
+
+ // Create a link and click on it.
+ const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+ link.href = blobUrl;
+ link.download = filename;
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(
+ 'click', true, false, window, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+ link.dispatchEvent(event);
+ },
+
+ onExportDisplayListClicked_() {
+ const rawData = JSON.stringify(this.displayItemList_.items);
+ this.saveFile_(this.displayListFilename_.value, rawData);
+ },
+
+ onExportSkPictureClicked_() {
+ const rawData = tr.b.Base64.atob(this.picture_.getBase64SkpData());
+ this.saveFile_(this.skpFilename_.value, rawData);
+ }
+ };
+
+ return {
+ DisplayItemDebugger,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html
new file mode 100644
index 00000000000..c10d6995db3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/display_item_list.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_debugger.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const displayItemList = new tr.e.cc.DisplayItemListSnapshot(
+ {id: '31415'},
+ 10,
+ {
+ 'params': {
+ 'layer_rect': [-15, -15, 46, 833],
+ 'items': [
+ 'BeginClipDisplayItem',
+ 'EndClipDisplayItem'
+ ]
+ },
+ 'skp64': '[another skia picture in base64]'});
+ displayItemList.preInitialize();
+ displayItemList.initialize();
+
+ const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger();
+ this.addHTMLOutput(dbg);
+ assert.isUndefined(dbg.displayItemList_);
+ assert.isUndefined(dbg.picture_);
+ dbg.displayItemList = displayItemList;
+ assert.isDefined(dbg.displayItemList_);
+ assert.isDefined(dbg.picture_);
+ assert.strictEqual(dbg.displayItemList_.items.length, 2);
+ dbg.style.border = '1px solid black';
+ });
+
+ test('selections', function() {
+ const displayItemList = new tr.e.cc.DisplayItemListSnapshot(
+ {id: '31415'},
+ 10,
+ {
+ 'params': {
+ 'layer_rect': [-15, -15, 46, 833],
+ 'items': [
+ 'BeginClipDisplayItem',
+ 'TransformDisplayItem',
+ {
+ 'name': 'DrawingDisplayItem',
+ 'skp64': '[skia picture in base64]',
+ },
+ 'EndTransformDisplayItem',
+ 'EndClipDisplayItem'
+ ]
+ },
+ 'skp64': '[another skia picture in base64]'});
+ displayItemList.preInitialize();
+ displayItemList.initialize();
+
+ const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger();
+ this.addHTMLOutput(dbg);
+ dbg.displayItemList = displayItemList;
+ assert.isDefined(dbg.displayItemList_);
+ assert.isDefined(dbg.picture_);
+ assert.strictEqual(dbg.displayItemList_.items.length, 5);
+
+ const initialPicture = dbg.picture_;
+ assert.isAbove(initialPicture.guid, 0);
+
+ // Select the drawing display item and make sure the picture updates.
+ const listView = dbg.displayItemListView_;
+ listView.selectedElement = listView.getElementByIndex(3);
+ let updatedPicture = dbg.picture_;
+ assert.isAbove(updatedPicture.guid, 0);
+ assert.notEqual(initialPicture.guid, updatedPicture.guid);
+
+ // Select the TransformDisplayItem and make sure the picture is blank.
+ listView.selectedElement = listView.getElementByIndex(2);
+ assert.isUndefined(dbg.picture_);
+
+ // Deselect a list item and make sure the picture is reset to the original.
+ listView.selectedElement = undefined;
+ updatedPicture = dbg.picture_;
+ assert.isAbove(updatedPicture.guid, 0);
+ assert.strictEqual(initialPicture.guid, updatedPicture.guid);
+
+ dbg.style.border = '1px solid black';
+ });
+
+ test('export', function() {
+ const displayItemList = new tr.e.cc.DisplayItemListSnapshot(
+ {id: '31415'},
+ 10,
+ {
+ 'params': {
+ 'layer_rect': [-15, -15, 46, 833],
+ 'items': [
+ 'BeginClipDisplayItem',
+ 'EndClipDisplayItem'
+ ]
+ },
+ 'skp64': 'c2twaWN0dXJl'});
+ displayItemList.preInitialize();
+ displayItemList.initialize();
+
+ const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger();
+ this.addHTMLOutput(dbg);
+ dbg.displayItemList = displayItemList;
+
+ let onSaveDisplayListCalled = false;
+ dbg.saveFile_ = function(filename, rawData) {
+ onSaveDisplayListCalled = true;
+ assert.strictEqual(filename, 'displayitemlist.json');
+ assert.strictEqual(
+ rawData, '["BeginClipDisplayItem","EndClipDisplayItem"]');
+ };
+ dbg.onExportDisplayListClicked_();
+ assert(onSaveDisplayListCalled);
+
+ let onSaveSkPictureCalled = false;
+ dbg.saveFile_ = function(filename, rawData) {
+ onSaveSkPictureCalled = true;
+ assert.strictEqual(filename, 'skpicture.skp');
+ assert.strictEqual(rawData, 'skpicture');
+ };
+ dbg.onExportSkPictureClicked_();
+ assert(onSaveSkPictureCalled);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html
new file mode 100644
index 00000000000..3024e8d2d22
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<!--
+An element displaying basic information about a display item in a list view.
+-->
+<dom-module id='tr-ui-e-chrome-cc-display-item-list-item'>
+ <template>
+ <style>
+ :host {
+ border-bottom: 1px solid #555;
+ display: block;
+ font-size: 12px;
+ padding: 3px 5px;
+ }
+
+ :host(:hover) {
+ background-color: #f0f0f0;
+ cursor: pointer;
+ }
+
+ .header {
+ font-weight: bold;
+ margin: 2px 0;
+ }
+
+ .header > .extra {
+ background-color: #777;
+ border-radius: 4px;
+ color: white;
+ margin: 0 6px;
+ text-decoration: none;
+ padding: 2px 4px;
+ }
+
+ .raw-details {
+ white-space: pre-wrap;
+ }
+
+ .details > dl {
+ margin: 0;
+ }
+
+ :host(:not([selected])) .details {
+ display: none;
+ }
+ </style>
+ <div class="header">
+ {{name}}
+ <template is="dom-if" if="{{_computeIfSKP(richDetails)}}">
+ <a class="extra" href$="{{_computeHref(richDetails)}}"
+ download="drawing.skp" on-click="{{stopPropagation}}">SKP</a>
+ </template>
+ </div>
+ <div class="details">
+ <template is="dom-if" if="{{rawDetails}}">
+ <div class="raw-details">{{rawDetails}}</div>
+ </template>
+ <template is="dom-if" if="{{richDetails}}">
+ <dl>
+ <template is="dom-if" if="{{richDetails.visualRect}}">
+ <dt>Visual rect</dt>
+ <dd>{{richDetails.visualRect.x}},{{richDetails.visualRect.y}}
+ {{richDetails.visualRect.width}}&times;{{richDetails.visualRect.height}}
+ </dd>
+ </template>
+ </dl>
+ </template>
+ </div>
+ </template>
+<script>
+'use strict';
+(function() {
+ // Extracts the "type" and "details" parts of the unstructured (plaintext)
+ // display item format, even if the details span multiple lines.
+ // For example, given "FooDisplayItem type=hello\nworld", produces
+ // "FooDisplayItem" as the first capture and "type=hello\nworld" as the
+ // second. Either capture could be the empty string, but this regex will
+ // still successfully match.
+ const DETAILS_SPLIT_REGEX = /^(\S*)\s*([\S\s]*)$/;
+
+ Polymer({
+ is: 'tr-ui-e-chrome-cc-display-item-list-item',
+
+ created() {
+ // TODO(charliea): Why is setAttribute necessary here but not below? We
+ // should reach out to the Polymer team to figure out.
+ Polymer.dom(this).setAttribute('name', '');
+ Polymer.dom(this).setAttribute('rawDetails', '');
+ Polymer.dom(this).setAttribute('richDetails', undefined);
+ Polymer.dom(this).setAttribute('data_', undefined);
+ },
+
+ get data() {
+ return this.data_;
+ },
+
+ set data(data) {
+ this.data_ = data;
+
+ if (!data) {
+ this.name = 'DATA MISSING';
+ this.rawDetails = '';
+ this.richDetails = undefined;
+ } else if (typeof data === 'string') {
+ const match = data.match(DETAILS_SPLIT_REGEX);
+ this.name = match[1];
+ this.rawDetails = match[2];
+ this.richDetails = undefined;
+ } else {
+ this.name = data.name;
+ this.rawDetails = '';
+ this.richDetails = data;
+ }
+ },
+
+ stopPropagation(e) {
+ e.stopPropagation();
+ },
+
+ _computeIfSKP(richDetails) {
+ return richDetails && richDetails.skp64;
+ },
+
+ _computeHref(richDetails) {
+ return 'data:application/octet-stream;base64,' + richDetails.skp64;
+ }
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html
new file mode 100644
index 00000000000..97598aaf3a7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/display_item_list.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_debugger.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /*
+ * Displays a display item snapshot in a human readable form.
+ * @constructor
+ */
+ const DisplayItemSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-display-item-list-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ DisplayItemSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ this.style.display = 'flex';
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.minWidth = 0;
+ this.displayItemDebugger_ = new tr.ui.e.chrome.cc.DisplayItemDebugger();
+ this.displayItemDebugger_.style.flexGrow = 1;
+ this.displayItemDebugger_.style.flexShrink = 1;
+ this.displayItemDebugger_.style.flexBasis = 'auto';
+ this.displayItemDebugger_.style.minWidth = 0;
+ Polymer.dom(this).appendChild(this.displayItemDebugger_);
+ },
+
+ updateContents() {
+ if (this.objectSnapshot_ && this.displayItemDebugger_) {
+ this.displayItemDebugger_.displayItemList = this.objectSnapshot_;
+ }
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ DisplayItemSnapshotView,
+ {
+ typeNames: ['cc::DisplayItemList'],
+ showInstances: false
+ });
+
+ return {
+ DisplayItemSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png
new file mode 100644
index 00000000000..a2b7710d3c4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg
new file mode 100644
index 00000000000..00531ac68d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="744.09448819"
+ height="1052.3622047"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="New document 1">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ id="filter3791">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.7246316"
+ id="feGaussianBlur3793" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="195.13782"
+ inkscape:cy="982.30556"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1215"
+ inkscape:window-height="860"
+ inkscape:window-x="2219"
+ inkscape:window-y="113"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="g3882"
+ style="opacity:0.5"
+ inkscape:export-filename="/tmp/input-event.png"
+ inkscape:export-xdpi="82.07"
+ inkscape:export-ydpi="82.07">
+ <path
+ transform="matrix(1.0152631,0,0,1.0152631,-0.71357503,0.46150497)"
+ sodipodi:type="arc"
+ style="opacity:0.50934604000000006;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter3791);enable-background:accumulate"
+ id="path3755"
+ sodipodi:cx="177.78685"
+ sodipodi:cy="100.79848"
+ sodipodi:rx="42.426407"
+ sodipodi:ry="42.426407"
+ d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" />
+ <path
+ transform="translate(-2,-2)"
+ d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z"
+ sodipodi:ry="42.426407"
+ sodipodi:rx="42.426407"
+ sodipodi:cy="100.79848"
+ sodipodi:cx="177.78685"
+ id="path2985"
+ style="color:#000000;fill:#d4d4d4;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3853"
+ d="m 175.28125,96.03125 0,8.46875 1,0 0,-8.46875 -1,0 z"
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3859"
+ d="m 171.53125,99.75 0,1 8.46875,0 0,-1 -8.46875,0 z"
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
+ </g>
+ <path
+ transform="matrix(1.2923213,0,0,1.2923213,-53.970887,-31.465544)"
+ d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z"
+ sodipodi:ry="42.426407"
+ sodipodi:rx="42.426407"
+ sodipodi:cy="100.79848"
+ sodipodi:cx="177.78685"
+ id="path3867"
+ style="color:#000000;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc"
+ inkscape:export-filename="/tmp/input-event.png"
+ inkscape:export-xdpi="82.07"
+ inkscape:export-ydpi="82.07" />
+ </g>
+</svg>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html
new file mode 100644
index 00000000000..9f81199e358
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html
@@ -0,0 +1,336 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const constants = tr.e.cc.constants;
+ const RENDER_PASS_QUADS =
+ Math.max(constants.ACTIVE_TREE, constants.PENDING_TREE) + 1;
+
+ /**
+ * @constructor
+ */
+ const LayerPicker = tr.ui.b.define('tr-ui-e-chrome-cc-layer-picker');
+
+ LayerPicker.prototype = {
+ __proto__: HTMLUnknownElement.prototype,
+
+ decorate() {
+ this.lthi_ = undefined;
+ this.controls_ = document.createElement('top-controls');
+ this.renderPassQuads_ = false;
+
+ this.style.display = 'flex';
+ this.style.flexDirection = 'column';
+ this.controls_.style.flexGrow = 0;
+ this.controls_.style.flexShrink = 0;
+ this.controls_.style.flexBasis = 'auto';
+ this.controls_.style.backgroundImage =
+ '-webkit-gradient(linear, 0 0, 100% 0, from(#E5E5E5), to(#D1D1D1))';
+ this.controls_.style.borderBottom = '1px solid #8e8e8e';
+ this.controls_.style.borderTop = '1px solid white';
+ this.controls_.style.display = 'inline';
+ this.controls_.style.fontSize = '14px';
+ this.controls_.style.paddingLeft = '2px';
+
+ this.itemList_ = new tr.ui.b.ListView();
+ this.itemList_.style.flexGrow = 1;
+ this.itemList_.style.flexShrink = 1;
+ this.itemList_.style.flexBasis = 'auto';
+ this.itemList_.style.fontFamily = 'monospace';
+ this.itemList_.style.overflow = 'auto';
+ Polymer.dom(this).appendChild(this.controls_);
+
+ Polymer.dom(this).appendChild(this.itemList_);
+
+ this.itemList_.addEventListener(
+ 'selection-changed', this.onItemSelectionChanged_.bind(this));
+
+ Polymer.dom(this.controls_).appendChild(tr.ui.b.createSelector(
+ this, 'whichTree',
+ 'layerPicker.whichTree', constants.ACTIVE_TREE,
+ [{label: 'Active tree', value: constants.ACTIVE_TREE},
+ {label: 'Pending tree', value: constants.PENDING_TREE},
+ {label: 'Render pass quads', value: RENDER_PASS_QUADS}]));
+
+ this.showPureTransformLayers_ = false;
+ const showPureTransformLayers = tr.ui.b.createCheckBox(
+ this, 'showPureTransformLayers',
+ 'layerPicker.showPureTransformLayers', false,
+ 'Transform layers');
+ Polymer.dom(showPureTransformLayers).classList.add(
+ 'show-transform-layers');
+ showPureTransformLayers.title =
+ 'When checked, pure transform layers are shown';
+ Polymer.dom(this.controls_).appendChild(showPureTransformLayers);
+ },
+
+ get lthiSnapshot() {
+ return this.lthiSnapshot_;
+ },
+
+ set lthiSnapshot(lthiSnapshot) {
+ this.lthiSnapshot_ = lthiSnapshot;
+ this.updateContents_();
+ },
+
+ get whichTree() {
+ return this.renderPassQuads_ ? constants.ACTIVE_TREE : this.whichTree_;
+ },
+
+ set whichTree(whichTree) {
+ this.whichTree_ = whichTree;
+ this.renderPassQuads_ = (whichTree === RENDER_PASS_QUADS);
+ this.updateContents_();
+ tr.b.dispatchSimpleEvent(this, 'selection-change', false);
+ },
+
+ get layerTreeImpl() {
+ if (this.lthiSnapshot === undefined) return undefined;
+
+ return this.lthiSnapshot.getTree(this.whichTree);
+ },
+
+ get isRenderPassQuads() {
+ return this.renderPassQuads_;
+ },
+
+ get showPureTransformLayers() {
+ return this.showPureTransformLayers_;
+ },
+
+ set showPureTransformLayers(show) {
+ if (this.showPureTransformLayers_ === show) return;
+
+ this.showPureTransformLayers_ = show;
+ this.updateContents_();
+ },
+
+ getRenderPassInfos_() {
+ if (!this.lthiSnapshot_) return [];
+
+ const renderPassInfo = [];
+ if (!this.lthiSnapshot_.args.frame ||
+ !this.lthiSnapshot_.args.frame.renderPasses) {
+ return renderPassInfo;
+ }
+
+ const renderPasses = this.lthiSnapshot_.args.frame.renderPasses;
+ for (let i = 0; i < renderPasses.length; ++i) {
+ const info = {renderPass: renderPasses[i],
+ depth: 0,
+ id: i,
+ name: 'cc::RenderPass'};
+ renderPassInfo.push(info);
+ }
+ return renderPassInfo;
+ },
+
+ getLayerInfos_() {
+ if (!this.lthiSnapshot_) return [];
+
+ const tree = this.lthiSnapshot_.getTree(this.whichTree_);
+ if (!tree) return [];
+
+ const layerInfos = [];
+
+ const showPureTransformLayers = this.showPureTransformLayers_;
+
+ const visitedLayers = {};
+ function visitLayer(layer, depth, isMask, isReplica) {
+ if (visitedLayers[layer.layerId]) return;
+
+ visitedLayers[layer.layerId] = true;
+ const info = {layer,
+ depth};
+
+ if (layer.args.drawsContent) {
+ info.name = layer.objectInstance.name;
+ } else {
+ info.name = 'cc::LayerImpl';
+ }
+
+ if (layer.usingGpuRasterization) {
+ info.name += ' (G)';
+ }
+
+ info.isMaskLayer = isMask;
+ info.replicaLayer = isReplica;
+
+ if (showPureTransformLayers || layer.args.drawsContent) {
+ layerInfos.push(info);
+ }
+ }
+ tree.iterLayers(visitLayer);
+ return layerInfos;
+ },
+
+ updateContents_() {
+ if (this.renderPassQuads_) {
+ this.updateRenderPassContents_();
+ } else {
+ this.updateLayerContents_();
+ }
+ },
+
+ updateRenderPassContents_() {
+ this.itemList_.clear();
+
+ let selectedRenderPassId;
+ if (this.selection_ && this.selection_.associatedRenderPassId) {
+ selectedRenderPassId = this.selection_.associatedRenderPassId;
+ }
+
+ const renderPassInfos = this.getRenderPassInfos_();
+ renderPassInfos.forEach(function(renderPassInfo) {
+ const renderPass = renderPassInfo.renderPass;
+ const id = renderPassInfo.id;
+
+ const item = this.createElementWithDepth_(renderPassInfo.depth);
+ const labelEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan());
+
+ Polymer.dom(labelEl).textContent = renderPassInfo.name + ' ' + id;
+ item.renderPass = renderPass;
+ item.renderPassId = id;
+ Polymer.dom(this.itemList_).appendChild(item);
+
+ if (id === selectedRenderPassId) {
+ renderPass.selectionState =
+ tr.model.SelectionState.SELECTED;
+ }
+ }, this);
+ },
+
+ updateLayerContents_() {
+ this.changingItemSelection_ = true;
+ try {
+ this.itemList_.clear();
+
+ let selectedLayerId;
+ if (this.selection_ && this.selection_.associatedLayerId) {
+ selectedLayerId = this.selection_.associatedLayerId;
+ }
+
+ const layerInfos = this.getLayerInfos_();
+ layerInfos.forEach(function(layerInfo) {
+ const layer = layerInfo.layer;
+ const id = layer.layerId;
+
+ const item = this.createElementWithDepth_(layerInfo.depth);
+ const labelEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan());
+
+ Polymer.dom(labelEl).textContent = layerInfo.name + ' ' + id;
+
+ const notesEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan());
+ if (layerInfo.isMaskLayer) {
+ Polymer.dom(notesEl).textContent += '(mask)';
+ }
+ if (layerInfo.isReplicaLayer) {
+ Polymer.dom(notesEl).textContent += '(replica)';
+ }
+
+ if ((layer.gpuMemoryUsageInBytes !== undefined) &&
+ (layer.gpuMemoryUsageInBytes > 0)) {
+ const gpuUsageStr = tr.b.Unit.byName.sizeInBytes.format(
+ layer.gpuMemoryUsageInBytes);
+ Polymer.dom(notesEl).textContent += ' (' + gpuUsageStr + ' MiB)';
+ }
+
+ item.layer = layer;
+ Polymer.dom(this.itemList_).appendChild(item);
+
+ if (layer.layerId === selectedLayerId) {
+ layer.selectionState = tr.model.SelectionState.SELECTED;
+ item.selected = true;
+ }
+ }, this);
+ } finally {
+ this.changingItemSelection_ = false;
+ }
+ },
+
+ createElementWithDepth_(depth) {
+ const item = document.createElement('div');
+
+ const indentEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan());
+ indentEl.style.whiteSpace = 'pre';
+ for (let i = 0; i < depth; i++) {
+ Polymer.dom(indentEl).textContent =
+ Polymer.dom(indentEl).textContent + ' ';
+ }
+ return item;
+ },
+
+ onItemSelectionChanged_(e) {
+ if (this.changingItemSelection_) return;
+ if (this.renderPassQuads_) {
+ this.onRenderPassSelected_(e);
+ } else {
+ this.onLayerSelected_(e);
+ }
+ tr.b.dispatchSimpleEvent(this, 'selection-change', false);
+ },
+
+ onRenderPassSelected_(e) {
+ let selectedRenderPass;
+ let selectedRenderPassId;
+ if (this.itemList_.selectedElement) {
+ selectedRenderPass = this.itemList_.selectedElement.renderPass;
+ selectedRenderPassId =
+ this.itemList_.selectedElement.renderPassId;
+ }
+
+ if (selectedRenderPass) {
+ this.selection_ = new tr.ui.e.chrome.cc.RenderPassSelection(
+ selectedRenderPass, selectedRenderPassId);
+ } else {
+ this.selection_ = undefined;
+ }
+ },
+
+ onLayerSelected_(e) {
+ let selectedLayer;
+ if (this.itemList_.selectedElement) {
+ selectedLayer = this.itemList_.selectedElement.layer;
+ }
+
+ if (selectedLayer) {
+ this.selection_ = new tr.ui.e.chrome.cc.LayerSelection(selectedLayer);
+ } else {
+ this.selection_ = undefined;
+ }
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ if (this.selection_ === selection) return;
+ this.selection_ = selection;
+ this.updateContents_();
+ }
+ };
+
+ return {
+ LayerPicker,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html
new file mode 100644
index 00000000000..1aaee9d7fbc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html">
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_picker.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /*
+ * Displays a LayerTreeHostImpl snapshot in a human readable form.
+ * @constructor
+ */
+ const LayerTreeHostImplSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-layer-tree-host-impl-snapshot-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ LayerTreeHostImplSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add('tr-ui-e-chrome-cc-lthi-s-view');
+ this.style.display = 'flex';
+ this.style.flexDirection = 'row';
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.minWidth = 0;
+
+ this.selection_ = undefined;
+
+ this.layerPicker_ = new tr.ui.e.chrome.cc.LayerPicker();
+ this.layerPicker_.style.flexGrow = 0;
+ this.layerPicker_.style.flexShrink = 0;
+ this.layerPicker_.style.flexBasis = 'auto';
+ this.layerPicker_.style.minWidth = '200px';
+ this.layerPicker_.addEventListener(
+ 'selection-change',
+ this.onLayerPickerSelectionChanged_.bind(this));
+
+ this.layerView_ = new tr.ui.e.chrome.cc.LayerView();
+ this.layerView_.addEventListener(
+ 'selection-change',
+ this.onLayerViewSelectionChanged_.bind(this));
+ this.layerView_.style.flexGrow = 1;
+ this.layerView_.style.flexShrink = 1;
+ this.layerView_.style.flexBasis = 'auto';
+ this.layerView_.style.minWidth = 0;
+
+ this.dragHandle_ = document.createElement('tr-ui-b-drag-handle');
+ this.dragHandle_.style.flexGrow = 0;
+ this.dragHandle_.style.flexShrink = 0;
+ this.dragHandle_.style.flexBasis = 'auto';
+ this.dragHandle_.horizontal = false;
+ this.dragHandle_.target = this.layerPicker_;
+
+ Polymer.dom(this).appendChild(this.layerPicker_);
+ Polymer.dom(this).appendChild(this.dragHandle_);
+ Polymer.dom(this).appendChild(this.layerView_);
+
+ // Make sure we have the current values from layerView_ and layerPicker_,
+ // since those might have been created before we added the listener.
+ this.onLayerViewSelectionChanged_();
+ this.onLayerPickerSelectionChanged_();
+ },
+
+ get objectSnapshot() {
+ return this.objectSnapshot_;
+ },
+
+ set objectSnapshot(objectSnapshot) {
+ this.objectSnapshot_ = objectSnapshot;
+
+ const lthi = this.objectSnapshot;
+ let layerTreeImpl;
+ if (lthi) {
+ layerTreeImpl = lthi.getTree(this.layerPicker_.whichTree);
+ }
+
+ this.layerPicker_.lthiSnapshot = lthi;
+ this.layerView_.layerTreeImpl = layerTreeImpl;
+ this.layerView_.regenerateContent();
+
+ if (!this.selection_) return;
+
+ this.selection = this.selection_.findEquivalent(lthi);
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ if (this.selection_ === selection) return;
+
+ this.selection_ = selection;
+ this.layerPicker_.selection = selection;
+ this.layerView_.selection = selection;
+ tr.b.dispatchSimpleEvent(this, 'cc-selection-change');
+ },
+
+ onLayerPickerSelectionChanged_() {
+ this.selection_ = this.layerPicker_.selection;
+ this.layerView_.selection = this.selection;
+ this.layerView_.layerTreeImpl = this.layerPicker_.layerTreeImpl;
+ this.layerView_.isRenderPassQuads = this.layerPicker_.isRenderPassQuads;
+ this.layerView_.regenerateContent();
+ tr.b.dispatchSimpleEvent(this, 'cc-selection-change');
+ },
+
+ onLayerViewSelectionChanged_() {
+ this.selection_ = this.layerView_.selection;
+ this.layerPicker_.selection = this.selection;
+ tr.b.dispatchSimpleEvent(this, 'cc-selection-change');
+ },
+
+ get extraHighlightsByLayerId() {
+ return this.layerView_.extraHighlightsByLayerId;
+ },
+
+ set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+ this.layerView_.extraHighlightsByLayerId = extraHighlightsByLayerId;
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ LayerTreeHostImplSnapshotView, {typeName: 'cc::LayerTreeHostImpl'});
+
+ return {
+ LayerTreeHostImplSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html
new file mode 100644
index 00000000000..1831be24618
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html">
+<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const snapshot = instance.snapshots[0];
+
+ const view = new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView();
+ view.style.width = '900px';
+ view.style.height = '400px';
+ view.objectSnapshot = snapshot;
+
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html
new file mode 100644
index 00000000000..2a7e5666f8b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html
@@ -0,0 +1,1200 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color.html">
+<link rel="import" href="/tracing/base/math/quad.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/unit_scale.html">
+<link rel="import" href="/tracing/extras/chrome/cc/debug_colors.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/extras/chrome/cc/render_pass.html">
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
+<link rel="import" href="/tracing/extras/chrome/cc/util.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_link.html">
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+<link rel="import" href="/tracing/ui/base/quad_stack_view.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+
+<template id='tr-ui-e-chrome-cc-layer-tree-quad-stack-view-template'>
+ <style>
+ #input-event {
+ background-image: url('./images/input-event.png');
+ display: none;
+ }
+ </style>
+ <img id='input-event'/>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Graphical view of LayerTreeImpl, with controls for
+ * type of layer content shown and info bar for content-loading warnings.
+ */
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ const THIS_DOC = document.currentScript.ownerDocument;
+ const TILE_HEATMAP_TYPE = {};
+ TILE_HEATMAP_TYPE.NONE = 'none';
+ TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY = 'scheduledPriority';
+ TILE_HEATMAP_TYPE.USING_GPU_MEMORY = 'usingGpuMemory';
+
+ const cc = tr.ui.e.chrome.cc;
+
+ function createTileRectsSelectorBaseOptions() {
+ return [{label: 'None', value: 'none'},
+ {label: 'Coverage Rects', value: 'coverage'}];
+ }
+
+
+ /**
+ * @constructor
+ */
+ const LayerTreeQuadStackView =
+ tr.ui.b.define('tr-ui-e-chrome-cc-layer-tree-quad-stack-view');
+
+ LayerTreeQuadStackView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.flexDirection = 'column';
+ this.style.minHeight = 0;
+ this.style.display = 'flex';
+
+ this.isRenderPassQuads_ = false;
+ this.pictureAsImageData_ = {}; // Maps picture.guid to PictureAsImageData.
+ this.messages_ = [];
+ this.controls_ = document.createElement('top-controls');
+ this.controls_.style.flexGrow = 0;
+ this.controls_.style.flexShrink = 0;
+ this.controls_.style.flexBasis = 'auto';
+ this.controls_.style.backgroundImage =
+ '-webkit-gradient(linear, 0 0, 100% 0, from(#E5E5E5), to(#D1D1D1))';
+ this.controls_.style.borderBottom = '1px solid #8e8e8e';
+ this.controls_.style.borderTop = '1px solid white';
+ this.controls_.style.display = 'flex';
+ this.controls_.style.flexDirection = 'row';
+ this.controls_.style.flexWrap = 'wrap';
+ this.controls_.style.fontSize = '14px';
+ this.controls_.style.paddingLeft = '2px';
+ this.controls_.style.overflow = 'hidden';
+ this.infoBar_ = document.createElement('tr-ui-b-info-bar');
+ this.quadStackView_ = new tr.ui.b.QuadStackView();
+ this.quadStackView_.addEventListener(
+ 'selectionchange', this.onQuadStackViewSelectionChange_.bind(this));
+ this.quadStackView_.style.flexGrow = 1;
+ this.quadStackView_.style.flexShrink = 1;
+ this.quadStackView_.style.flexBasis = 'auto';
+ this.quadStackView_.style.minWidth = '200px';
+
+ this.extraHighlightsByLayerId_ = undefined;
+ this.inputEventImageData_ = undefined;
+
+ const m = tr.ui.b.MOUSE_SELECTOR_MODE;
+ const mms = this.quadStackView_.mouseModeSelector;
+ mms.settingsKey = 'tr.e.cc.layerTreeQuadStackView.mouseModeSelector';
+ mms.setKeyCodeForMode(m.SELECTION, 'Z'.charCodeAt(0));
+ mms.setKeyCodeForMode(m.PANSCAN, 'X'.charCodeAt(0));
+ mms.setKeyCodeForMode(m.ZOOM, 'C'.charCodeAt(0));
+ mms.setKeyCodeForMode(m.ROTATE, 'V'.charCodeAt(0));
+
+ const node = tr.ui.b.instantiateTemplate(
+ '#tr-ui-e-chrome-cc-layer-tree-quad-stack-view-template', THIS_DOC);
+ Polymer.dom(this).appendChild(node);
+ Polymer.dom(this).appendChild(this.controls_);
+ Polymer.dom(this).appendChild(this.infoBar_);
+ Polymer.dom(this).appendChild(this.quadStackView_);
+
+ this.tileRectsSelector_ = tr.ui.b.createSelector(
+ this, 'howToShowTiles',
+ 'layerView.howToShowTiles', 'none',
+ createTileRectsSelectorBaseOptions());
+ Polymer.dom(this.controls_).appendChild(this.tileRectsSelector_);
+
+ const tileHeatmapText = tr.ui.b.createSpan({
+ textContent: 'Tile heatmap:'
+ });
+ Polymer.dom(this.controls_).appendChild(tileHeatmapText);
+
+ const tileHeatmapSelector = tr.ui.b.createSelector(
+ this, 'tileHeatmapType',
+ 'layerView.tileHeatmapType', TILE_HEATMAP_TYPE.NONE,
+ [{label: 'None',
+ value: TILE_HEATMAP_TYPE.NONE},
+ {label: 'Scheduled Priority',
+ value: TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY},
+ {label: 'Is using GPU memory',
+ value: TILE_HEATMAP_TYPE.USING_GPU_MEMORY}
+ ]);
+ Polymer.dom(this.controls_).appendChild(tileHeatmapSelector);
+
+ const showOtherLayersCheckbox = tr.ui.b.createCheckBox(
+ this, 'showOtherLayers',
+ 'layerView.showOtherLayers', true,
+ 'Other layers/passes');
+ showOtherLayersCheckbox.title =
+ 'When checked, show all layers, selected or not.';
+ Polymer.dom(this.controls_).appendChild(showOtherLayersCheckbox);
+
+ const showInvalidationsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showInvalidations',
+ 'layerView.showInvalidations', true,
+ 'Invalidations');
+ showInvalidationsCheckbox.title =
+ 'When checked, compositing invalidations are highlighted in red';
+ Polymer.dom(this.controls_).appendChild(showInvalidationsCheckbox);
+
+ const showUnrecordedRegionCheckbox = tr.ui.b.createCheckBox(
+ this, 'showUnrecordedRegion',
+ 'layerView.showUnrecordedRegion', true,
+ 'Unrecorded area');
+ showUnrecordedRegionCheckbox.title =
+ 'When checked, unrecorded areas are highlighted in yellow';
+ Polymer.dom(this.controls_).appendChild(showUnrecordedRegionCheckbox);
+
+ const showBottlenecksCheckbox = tr.ui.b.createCheckBox(
+ this, 'showBottlenecks',
+ 'layerView.showBottlenecks', true,
+ 'Bottlenecks');
+ showBottlenecksCheckbox.title =
+ 'When checked, scroll bottlenecks are highlighted';
+ Polymer.dom(this.controls_).appendChild(showBottlenecksCheckbox);
+
+ const showLayoutRectsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showLayoutRects',
+ 'layerView.showLayoutRects', false,
+ 'Layout rects');
+ showLayoutRectsCheckbox.title =
+ 'When checked, shows rects for regions where layout happened';
+ Polymer.dom(this.controls_).appendChild(showLayoutRectsCheckbox);
+
+ const showContentsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showContents',
+ 'layerView.showContents', true,
+ 'Contents');
+ showContentsCheckbox.title =
+ 'When checked, show the rendered contents inside the layer outlines';
+ Polymer.dom(this.controls_).appendChild(showContentsCheckbox);
+
+ const showAnimationBoundsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showAnimationBounds',
+ 'layerView.showAnimationBounds', false,
+ 'Animation Bounds');
+ showAnimationBoundsCheckbox.title = 'When checked, show a border around' +
+ ' a layer showing the extent of its animation.';
+ Polymer.dom(this.controls_).appendChild(showAnimationBoundsCheckbox);
+
+ const showInputEventsCheckbox = tr.ui.b.createCheckBox(
+ this, 'showInputEvents',
+ 'layerView.showInputEvents', true,
+ 'Input events');
+ showInputEventsCheckbox.title = 'When checked, input events are ' +
+ 'displayed as circles.';
+ Polymer.dom(this.controls_).appendChild(showInputEventsCheckbox);
+
+ this.whatRasterizedLink_ = document.createElement(
+ 'tr-ui-a-analysis-link');
+ this.whatRasterizedLink_.style.position = 'absolute';
+ this.whatRasterizedLink_.style.bottom = '15px';
+ this.whatRasterizedLink_.style.left = '10px';
+ this.whatRasterizedLink_.selection =
+ this.getWhatRasterizedEventSet_.bind(this);
+ Polymer.dom(this.quadStackView_).appendChild(this.whatRasterizedLink_);
+ },
+
+ get layerTreeImpl() {
+ return this.layerTreeImpl_;
+ },
+
+ set isRenderPassQuads(newValue) {
+ this.isRenderPassQuads_ = newValue;
+ },
+
+ set layerTreeImpl(layerTreeImpl) {
+ if (this.layerTreeImpl_ === layerTreeImpl) return;
+
+ // FIXME(pdr): We may want to clear pictureAsImageData_ here to save
+ // memory at the cost of performance. Note that
+ // pictureAsImageData_ will be cleared when this is
+ // destructed, but this view might live for several
+ // layerTreeImpls.
+ this.layerTreeImpl_ = layerTreeImpl;
+ this.selection = undefined;
+ },
+
+ get extraHighlightsByLayerId() {
+ return this.extraHighlightsByLayerId_;
+ },
+
+ set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+ this.extraHighlightsByLayerId_ = extraHighlightsByLayerId;
+ this.scheduleUpdateContents_();
+ },
+
+ get showOtherLayers() {
+ return this.showOtherLayers_;
+ },
+
+ set showOtherLayers(show) {
+ this.showOtherLayers_ = show;
+ this.updateContents_();
+ },
+
+ get showAnimationBounds() {
+ return this.showAnimationBounds_;
+ },
+
+ set showAnimationBounds(show) {
+ this.showAnimationBounds_ = show;
+ this.updateContents_();
+ },
+
+ get showInputEvents() {
+ return this.showInputEvents_;
+ },
+
+ set showInputEvents(show) {
+ this.showInputEvents_ = show;
+ this.updateContents_();
+ },
+
+ get showContents() {
+ return this.showContents_;
+ },
+
+ set showContents(show) {
+ this.showContents_ = show;
+ this.updateContents_();
+ },
+
+ get showInvalidations() {
+ return this.showInvalidations_;
+ },
+
+ set showInvalidations(show) {
+ this.showInvalidations_ = show;
+ this.updateContents_();
+ },
+
+ get showUnrecordedRegion() {
+ return this.showUnrecordedRegion_;
+ },
+
+ set showUnrecordedRegion(show) {
+ this.showUnrecordedRegion_ = show;
+ this.updateContents_();
+ },
+
+ get showBottlenecks() {
+ return this.showBottlenecks_;
+ },
+
+ set showBottlenecks(show) {
+ this.showBottlenecks_ = show;
+ this.updateContents_();
+ },
+
+ get showLayoutRects() {
+ return this.showLayoutRects_;
+ },
+
+ set showLayoutRects(show) {
+ this.showLayoutRects_ = show;
+ this.updateContents_();
+ },
+
+ get howToShowTiles() {
+ return this.howToShowTiles_;
+ },
+
+ set howToShowTiles(val) {
+ // Make sure val is something we expect.
+ if (val !== 'none' && val !== 'coverage' && isNaN(parseFloat(val))) {
+ throw new Error(
+ 'howToShowTiles requires "none" or "coverage" or a number');
+ }
+
+ this.howToShowTiles_ = val;
+ this.updateContents_();
+ },
+
+ get tileHeatmapType() {
+ return this.tileHeatmapType_;
+ },
+
+ set tileHeatmapType(val) {
+ this.tileHeatmapType_ = val;
+ this.updateContents_();
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ if (this.selection === selection) return;
+
+ this.selection_ = selection;
+ tr.b.dispatchSimpleEvent(this, 'selection-change');
+ this.updateContents_();
+ },
+
+ regenerateContent() {
+ this.updateTilesSelector_();
+ this.updateContents_();
+ },
+
+ loadDataForImageElement_(image, callback) {
+ const imageContent = window.getComputedStyle(image).backgroundImage;
+ if (!imageContent) {
+ // The style has not been applied because the view has not been added
+ // into the DOM tree yet. Try again in another cycle.
+ this.scheduleUpdateContents_();
+ return;
+ }
+ image.src = tr.ui.b.extractUrlString(imageContent);
+ image.onload = function() {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ canvas.width = image.width;
+ canvas.height = image.height;
+ ctx.drawImage(image, 0, 0);
+ const imageData = ctx.getImageData(
+ 0, 0, canvas.width, canvas.height);
+ callback(imageData);
+ };
+ },
+
+ onQuadStackViewSelectionChange_(e) {
+ const selectableQuads = e.quads.filter(function(q) {
+ return q.selectionToSetIfClicked !== undefined;
+ });
+ if (selectableQuads.length === 0) {
+ this.selection = undefined;
+ return;
+ }
+
+ // Sort the quads low to high on stackingGroupId.
+ selectableQuads.sort(function(x, y) {
+ const z = x.stackingGroupId - y.stackingGroupId;
+ if (z !== 0) return z;
+
+ return x.selectionToSetIfClicked.specicifity -
+ y.selectionToSetIfClicked.specicifity;
+ });
+
+ // TODO(nduca): Support selecting N things at once.
+ const quadToSelect = selectableQuads[selectableQuads.length - 1];
+ this.selection = quadToSelect.selectionToSetIfClicked;
+ },
+
+ scheduleUpdateContents_() {
+ if (this.updateContentsPending_) return;
+
+ this.updateContentsPending_ = true;
+ tr.b.requestAnimationFrameInThisFrameIfPossible(
+ this.updateContents_, this);
+ },
+
+ updateContents_() {
+ if (!this.layerTreeImpl_) {
+ this.quadStackView_.headerText = 'No tree';
+ this.quadStackView_.quads = [];
+ return;
+ }
+
+
+ const status = this.computePictureLoadingStatus_();
+ if (!status.picturesComplete) return;
+
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+ const lthiInstance = lthi.objectInstance;
+ const worldViewportRect = tr.b.math.Rect.fromXYWH(
+ 0, 0,
+ lthi.deviceViewportSize.width, lthi.deviceViewportSize.height);
+ this.quadStackView_.deviceRect = worldViewportRect;
+ if (this.isRenderPassQuads_) {
+ this.quadStackView_.quads = this.generateRenderPassQuads();
+ } else {
+ this.quadStackView_.quads = this.generateLayerQuads();
+ }
+
+ this.updateWhatRasterizedLinkState_();
+
+ let message = '';
+ if (lthi.tilesHaveGpuMemoryUsageInfo) {
+ const thisTreeUsageInBytes = this.layerTreeImpl_.gpuMemoryUsageInBytes;
+ const otherTreeUsageInBytes = lthi.gpuMemoryUsageInBytes -
+ thisTreeUsageInBytes;
+ message +=
+ tr.b.convertUnit(thisTreeUsageInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB on this tree';
+ if (otherTreeUsageInBytes) {
+ message += ', ' +
+ tr.b.convertUnit(otherTreeUsageInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB on the other tree';
+ }
+ } else {
+ if (this.layerTreeImpl_) {
+ const thisTreeUsageInBytes =
+ this.layerTreeImpl_.gpuMemoryUsageInBytes;
+ message +=
+ tr.b.convertUnit(thisTreeUsageInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB on this tree';
+
+ if (this.layerTreeImpl_.otherTree) {
+ // Older Chromes don't report enough data to know how much memory is
+ // being used across both trees. We know the memory consumed by each
+ // tree, but there is resource sharing *between the trees* so we
+ // can't simply sum up the per-tree costs. We need either the total
+ // plus one tree, to guess the unique on the other tree, etc. Newer
+ // chromes report memory per tile, which allows LTHI to compute the
+ // total tile memory usage, letting us figure things out properly.
+ message += ', ??? MiB on other tree. ';
+ }
+ }
+ }
+
+ if (lthi.args.tileManagerBasicState) {
+ const tmgs = lthi.args.tileManagerBasicState.globalState;
+ message += ' (softMax=' +
+ tr.b.convertUnit(tmgs.softMemoryLimitInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB, hardMax=' +
+ tr.b.convertUnit(tmgs.hardMemoryLimitInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + ' MiB, ' +
+ tmgs.memoryLimitPolicy + ')';
+ } else {
+ // Old Chromes do not have a globalState on the LTHI dump.
+ // But they do issue a DidManage event wiht the globalstate. Find that
+ // event so that we show some global state.
+ const thread = lthi.snapshottedOnThread;
+ const didManageTilesSlices = thread.sliceGroup.slices.filter(s => {
+ if (s.category !== 'tr.e.cc') return false;
+
+ if (s.title !== 'DidManage') return false;
+
+ if (s.end > lthi.ts) return false;
+
+ return true;
+ });
+ didManageTilesSlices.sort(function(x, y) {
+ return x.end - y.end;
+ });
+ if (didManageTilesSlices.length > 0) {
+ const newest = didManageTilesSlices[didManageTilesSlices.length - 1];
+ const tmgs = newest.args.state.global_state;
+ message += ' (softMax=' +
+ tr.b.convertUnit(tmgs.softMemoryLimitInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) +
+ ' MiB, hardMax=' +
+ tr.b.convertUnit(tmgs.hardMemoryLimitInBytes,
+ tr.b.UnitPrefixScale.BINARY.NONE,
+ tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + ' MiB, ' +
+ tmgs.memoryLimitPolicy + ')';
+ }
+ }
+
+ if (this.layerTreeImpl_.otherTree) {
+ message += ' (Another tree exists)';
+ }
+
+ if (message.length) {
+ this.quadStackView_.headerText = message;
+ } else {
+ this.quadStackView_.headerText = undefined;
+ }
+
+ this.updateInfoBar_(status.messages);
+ },
+
+ updateTilesSelector_() {
+ const data = createTileRectsSelectorBaseOptions();
+
+ if (this.layerTreeImpl_) {
+ // First get all of the scales information from LTHI.
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+ const scaleNames = lthi.getContentsScaleNames();
+ for (const scale in scaleNames) {
+ data.push({
+ label: 'Scale ' + scale + ' (' + scaleNames[scale] + ')',
+ value: scale
+ });
+ }
+ }
+
+ // Then create a new selector and replace the old one.
+ const newSelector = tr.ui.b.createSelector(
+ this, 'howToShowTiles',
+ 'layerView.howToShowTiles', 'none',
+ data);
+ this.controls_.replaceChild(newSelector, this.tileRectsSelector_);
+ this.tileRectsSelector_ = newSelector;
+ },
+
+ computePictureLoadingStatus_() {
+ // Figure out if we can draw the quads yet. While we're at it, figure out
+ // if we have any warnings we need to show.
+ const layers = this.layers;
+ const status = {
+ messages: [],
+ picturesComplete: true
+ };
+ if (this.showContents) {
+ let hasPendingRasterizeImage = false;
+ let firstPictureError = undefined;
+ let hasMissingLayerRect = false;
+ let hasUnresolvedPictureRef = false;
+ for (let i = 0; i < layers.length; i++) {
+ const layer = layers[i];
+ for (let ir = 0; ir < layer.pictures.length; ++ir) {
+ const picture = layer.pictures[ir];
+
+ if (picture.idRef) {
+ hasUnresolvedPictureRef = true;
+ continue;
+ }
+ if (!picture.layerRect) {
+ hasMissingLayerRect = true;
+ continue;
+ }
+
+ const pictureAsImageData = this.pictureAsImageData_[picture.guid];
+ if (!pictureAsImageData) {
+ hasPendingRasterizeImage = true;
+ this.pictureAsImageData_[picture.guid] =
+ tr.e.cc.PictureAsImageData.Pending(this);
+ picture.rasterize(
+ {stopIndex: undefined},
+ function(pictureImageData) {
+ const picture_ = pictureImageData.picture;
+ this.pictureAsImageData_[picture_.guid] = pictureImageData;
+ this.scheduleUpdateContents_();
+ }.bind(this));
+ continue;
+ }
+ if (pictureAsImageData.isPending()) {
+ hasPendingRasterizeImage = true;
+ continue;
+ }
+ if (pictureAsImageData.error) {
+ if (!firstPictureError) {
+ firstPictureError = pictureAsImageData.error;
+ }
+ break;
+ }
+ }
+ }
+ if (hasPendingRasterizeImage) {
+ status.picturesComplete = false;
+ } else {
+ if (hasUnresolvedPictureRef) {
+ status.messages.push({
+ header: 'Missing picture',
+ details: 'Your trace didn\'t have pictures for every layer. ' +
+ 'Old chrome versions had this problem'});
+ }
+ if (hasMissingLayerRect) {
+ status.messages.push({
+ header: 'Missing layer rect',
+ details: 'Your trace may be corrupt or from a very old ' +
+ 'Chrome revision.'});
+ }
+ if (firstPictureError) {
+ status.messages.push({
+ header: 'Cannot rasterize',
+ details: firstPictureError});
+ }
+ }
+ }
+ if (this.showInputEvents && this.layerTreeImpl.tracedInputLatencies &&
+ this.inputEventImageData_ === undefined) {
+ const image = Polymer.dom(this).querySelector('#input-event');
+ if (!image.src) {
+ this.loadDataForImageElement_(image, function(imageData) {
+ this.inputEventImageData_ = imageData;
+ this.updateContentsPending_ = false;
+ this.scheduleUpdateContents_();
+ }.bind(this));
+ }
+ status.picturesComplete = false;
+ }
+ return status;
+ },
+
+ get selectedRenderPass() {
+ if (this.selection) {
+ return this.selection.renderPass_;
+ }
+ },
+
+ get selectedLayer() {
+ if (this.selection) {
+ const selectedLayerId = this.selection.associatedLayerId;
+ return this.layerTreeImpl_.findLayerWithId(selectedLayerId);
+ }
+ },
+
+ get renderPasses() {
+ let renderPasses =
+ this.layerTreeImpl.layerTreeHostImpl.args.frame.renderPasses;
+ if (!this.showOtherLayers) {
+ const selectedRenderPass = this.selectedRenderPass;
+ if (selectedRenderPass) {
+ renderPasses = [selectedRenderPass];
+ }
+ }
+ return renderPasses;
+ },
+
+ get layers() {
+ let layers = this.layerTreeImpl.renderSurfaceLayerList;
+ if (!this.showOtherLayers) {
+ const selectedLayer = this.selectedLayer;
+ if (selectedLayer) {
+ layers = [selectedLayer];
+ }
+ }
+ return layers;
+ },
+
+ appendImageQuads_(quads, layer, layerQuad) {
+ // Generate image quads for the layer
+ for (let ir = 0; ir < layer.pictures.length; ++ir) {
+ const picture = layer.pictures[ir];
+ if (!picture.layerRect) continue;
+
+ const unitRect = picture.layerRect.asUVRectInside(layer.bounds);
+ const iq = layerQuad.projectUnitRect(unitRect);
+
+ const pictureData = this.pictureAsImageData_[picture.guid];
+ if (this.showContents && pictureData && pictureData.imageData) {
+ iq.imageData = pictureData.imageData;
+ iq.borderColor = 'rgba(0,0,0,0)';
+ } else {
+ iq.imageData = undefined;
+ }
+
+ iq.stackingGroupId = layerQuad.stackingGroupId;
+ quads.push(iq);
+ }
+ },
+
+ appendAnimationQuads_(quads, layer, layerQuad) {
+ if (!layer.animationBoundsRect) return;
+
+ const rect = layer.animationBoundsRect;
+ const abq = tr.b.math.Quad.fromRect(rect);
+
+ abq.backgroundColor = 'rgba(164,191,48,0.5)';
+ abq.borderColor = 'rgba(205,255,0,0.75)';
+ abq.borderWidth = 3.0;
+ abq.stackingGroupId = layerQuad.stackingGroupId;
+ abq.selectionToSetIfClicked = new cc.AnimationRectSelection(
+ layer, rect);
+ quads.push(abq);
+ },
+
+ appendInvalidationQuads_(quads, layer, layerQuad) {
+ if (layer.layerTreeImpl.hasSourceFrameBeenDrawnBefore) return;
+
+ // Generate the invalidation rect quads.
+ for (const rect of layer.invalidation.rects) {
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const iq = layerQuad.projectUnitRect(unitRect);
+ iq.backgroundColor = 'rgba(0, 255, 0, 0.1)';
+ if (rect.reason === 'appeared') {
+ iq.backgroundColor = 'rgba(0, 255, 128, 0.1)';
+ }
+ iq.borderColor = 'rgba(0, 255, 0, 1)';
+ iq.stackingGroupId = layerQuad.stackingGroupId;
+
+ let message = 'Invalidation rect';
+ if (rect.reason) {
+ message += ' (' + rect.reason + ')';
+ }
+ if (rect.client) {
+ message += ' for ' + rect.client;
+ }
+
+ iq.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, message, rect, rect);
+ quads.push(iq);
+ }
+ },
+
+ appendUnrecordedRegionQuads_(quads, layer, layerQuad) {
+ // Generate the unrecorded region quads.
+ for (let ir = 0; ir < layer.unrecordedRegion.rects.length; ir++) {
+ const rect = layer.unrecordedRegion.rects[ir];
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const iq = layerQuad.projectUnitRect(unitRect);
+ iq.backgroundColor = 'rgba(240, 230, 140, 0.3)';
+ iq.borderColor = 'rgba(240, 230, 140, 1)';
+ iq.stackingGroupId = layerQuad.stackingGroupId;
+ iq.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, 'Unrecorded area', rect, rect);
+ quads.push(iq);
+ }
+ },
+
+ appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId) {
+ function processRegion(region, label, borderColor) {
+ const backgroundColor = borderColor.clone();
+ backgroundColor.a = 0.4 * (borderColor.a || 1.0);
+
+ if (!region || !region.rects) return;
+
+ for (let ir = 0; ir < region.rects.length; ir++) {
+ const rect = region.rects[ir];
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const iq = layerQuad.projectUnitRect(unitRect);
+ iq.backgroundColor = backgroundColor.toString();
+ iq.borderColor = borderColor.toString();
+ iq.borderWidth = 4.0;
+ iq.stackingGroupId = stackingGroupId;
+ iq.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, label, rect, rect);
+ quads.push(iq);
+ }
+ }
+
+ processRegion(layer.touchEventHandlerRegion, 'Touch listener',
+ tr.b.Color.fromString('rgb(228, 226, 27)'));
+ processRegion(layer.wheelEventHandlerRegion, 'Wheel listener',
+ tr.b.Color.fromString('rgb(176, 205, 29)'));
+ processRegion(layer.nonFastScrollableRegion, 'Repaints on scroll',
+ tr.b.Color.fromString('rgb(213, 134, 32)'));
+ },
+
+ appendTileCoverageRectQuads_(
+ quads, layer, layerQuad, heatmapType) {
+ if (!layer.tileCoverageRects) return;
+
+ const tiles = [];
+ for (let ct = 0; ct < layer.tileCoverageRects.length; ++ct) {
+ const tile = layer.tileCoverageRects[ct].tile;
+ if (tile !== undefined) tiles.push(tile);
+ }
+
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+ const minMax =
+ this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType);
+ const heatmapResult =
+ this.computeHeatmapColors_(tiles, minMax, heatmapType);
+ let heatIndex = 0;
+
+ for (let ct = 0; ct < layer.tileCoverageRects.length; ++ct) {
+ let rect = layer.tileCoverageRects[ct].geometryRect;
+ rect = rect.scale(1.0 / layer.geometryContentsScale);
+
+ const tile = layer.tileCoverageRects[ct].tile;
+
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const quad = layerQuad.projectUnitRect(unitRect);
+
+ quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+ quad.stackingGroupId = layerQuad.stackingGroupId;
+ let type = tr.e.cc.tileTypes.missing;
+ if (tile) {
+ type = tile.getTypeForLayer(layer);
+ quad.backgroundColor = heatmapResult[heatIndex].color;
+ ++heatIndex;
+ }
+
+ quad.borderColor = tr.e.cc.tileBorder[type].color;
+ quad.borderWidth = tr.e.cc.tileBorder[type].width;
+ let label;
+ if (tile) {
+ label = 'coverageRect';
+ } else {
+ label = 'checkerboard coverageRect';
+ }
+ quad.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, label, rect, layer.tileCoverageRects[ct]);
+
+ quads.push(quad);
+ }
+ },
+
+ appendLayoutRectQuads_(quads, layer, layerQuad) {
+ if (!layer.layoutRects) {
+ return;
+ }
+
+ for (let ct = 0; ct < layer.layoutRects.length; ++ct) {
+ let rect = layer.layoutRects[ct].geometryRect;
+ rect = rect.scale(1.0 / layer.geometryContentsScale);
+
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const quad = layerQuad.projectUnitRect(unitRect);
+
+ quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+ quad.stackingGroupId = layerQuad.stackingGroupId;
+
+ quad.borderColor = 'rgba(0, 0, 200, 0.7)';
+ quad.borderWidth = 2;
+ const label = 'Layout rect';
+ quad.selectionToSetIfClicked = new cc.LayerRectSelection(
+ layer, label, rect);
+
+ quads.push(quad);
+ }
+ },
+
+ getValueForHeatmap_(tile, heatmapType) {
+ if (heatmapType === TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY) {
+ return tile.scheduledPriority === 0 ?
+ undefined :
+ tile.scheduledPriority;
+ } else if (heatmapType === TILE_HEATMAP_TYPE.USING_GPU_MEMORY) {
+ if (tile.isSolidColor) return 0.5;
+ return tile.isUsingGpuMemory ? 0 : 1;
+ }
+ },
+
+ getMinMaxForHeatmap_(tiles, heatmapType) {
+ const range = new tr.b.math.Range();
+ if (heatmapType === TILE_HEATMAP_TYPE.USING_GPU_MEMORY) {
+ range.addValue(0);
+ range.addValue(1);
+ return range;
+ }
+
+ for (let i = 0; i < tiles.length; ++i) {
+ const value = this.getValueForHeatmap_(tiles[i], heatmapType);
+ if (value === undefined) continue;
+ range.addValue(value);
+ }
+ if (range.range === 0) {
+ range.addValue(1);
+ }
+ return range;
+ },
+
+ computeHeatmapColors_(tiles, minMax, heatmapType) {
+ const min = minMax.min;
+ const max = minMax.max;
+
+ const color = function(value) {
+ let hue = 120 * (1 - (value - min) / (max - min));
+ if (hue < 0) hue = 0;
+ return 'hsla(' + hue + ', 100%, 50%, 0.5)';
+ };
+
+ const values = [];
+ for (let i = 0; i < tiles.length; ++i) {
+ const tile = tiles[i];
+ const value = this.getValueForHeatmap_(tile, heatmapType);
+ const res = {
+ value,
+ color: value !== undefined ? color(value) : undefined
+ };
+ values.push(res);
+ }
+
+ return values;
+ },
+
+ appendTilesWithScaleQuads_(
+ quads, layer, layerQuad, scale, heatmapType) {
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+
+ const tiles = [];
+ for (let i = 0; i < lthi.activeTiles.length; ++i) {
+ const tile = lthi.activeTiles[i];
+
+ if (Math.abs(tile.contentsScale - scale) > 1e-6) {
+ continue;
+ }
+
+ // TODO(vmpstr): Make the stiching of tiles and layers a part of
+ // tile construction (issue 346)
+ if (layer.layerId !== tile.layerId) continue;
+
+ tiles.push(tile);
+ }
+
+ const minMax =
+ this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType);
+ const heatmapResult =
+ this.computeHeatmapColors_(tiles, minMax, heatmapType);
+
+ for (let i = 0; i < tiles.length; ++i) {
+ const tile = tiles[i];
+ const rect = tile.layerRect;
+ if (!tile.layerRect) continue;
+
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const quad = layerQuad.projectUnitRect(unitRect);
+
+ quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+ quad.stackingGroupId = layerQuad.stackingGroupId;
+
+ const type = tile.getTypeForLayer(layer);
+ quad.borderColor = tr.e.cc.tileBorder[type].color;
+ quad.borderWidth = tr.e.cc.tileBorder[type].width;
+
+ quad.backgroundColor = heatmapResult[i].color;
+ const data = {
+ tileType: type
+ };
+ if (heatmapType !== TILE_HEATMAP_TYPE.NONE) {
+ data[heatmapType] = heatmapResult[i].value;
+ }
+ quad.selectionToSetIfClicked = new cc.TileSelection(tile, data);
+ quads.push(quad);
+ }
+ },
+
+ appendHighlightQuadsForLayer_(
+ quads, layer, layerQuad, highlights) {
+ highlights.forEach(function(highlight) {
+ const rect = highlight.rect;
+
+ const unitRect = rect.asUVRectInside(layer.bounds);
+ const quad = layerQuad.projectUnitRect(unitRect);
+
+ let colorId = ColorScheme.getColorIdForGeneralPurposeString(
+ highlight.colorKey);
+ const offset = ColorScheme.properties.brightenedOffsets[0];
+ colorId = ColorScheme.getVariantColorId(colorId, offset);
+
+ const color = ColorScheme.colors[colorId];
+
+ const quadForDrawing = quad.clone();
+ quadForDrawing.backgroundColor = color.withAlpha(0.5).toString();
+ quadForDrawing.borderColor = color.withAlpha(1.0).darken().toString();
+ quadForDrawing.stackingGroupId = layerQuad.stackingGroupId;
+ quads.push(quadForDrawing);
+ }, this);
+ },
+
+ generateRenderPassQuads() {
+ if (!this.layerTreeImpl.layerTreeHostImpl.args.frame) return [];
+ const renderPasses = this.renderPasses;
+ if (!renderPasses) return [];
+
+ const quads = [];
+ for (let i = 0; i < renderPasses.length; ++i) {
+ const quadList = renderPasses[i].quadList;
+ for (let j = 0; j < quadList.length; ++j) {
+ const drawQuad = quadList[j];
+ const quad = drawQuad.rectAsTargetSpaceQuad.clone();
+ quad.borderColor = 'rgb(170, 204, 238)';
+ quad.borderWidth = 2;
+ quad.stackingGroupId = i;
+ quads.push(quad);
+ }
+ }
+ return quads;
+ },
+
+ generateLayerQuads() {
+ this.updateContentsPending_ = false;
+
+ // Generate the quads for the view.
+ const layers = this.layers;
+ const quads = [];
+ let nextStackingGroupId = 0;
+ const alreadyVisitedLayerIds = {};
+
+
+ let selectionHighlightsByLayerId;
+ if (this.selection) {
+ selectionHighlightsByLayerId = this.selection.highlightsByLayerId;
+ } else {
+ selectionHighlightsByLayerId = {};
+ }
+
+ const extraHighlightsByLayerId = this.extraHighlightsByLayerId || {};
+
+ for (let i = 1; i <= layers.length; i++) {
+ // Generate quads back-to-front.
+ const layer = layers[layers.length - i];
+ alreadyVisitedLayerIds[layer.layerId] = true;
+ if (layer.objectInstance.name === 'cc::NinePatchLayerImpl') {
+ continue;
+ }
+
+ const layerQuad = layer.layerQuad.clone();
+ if (layer.usingGpuRasterization) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ layerQuad.borderWidth = 2.0 * pixelRatio;
+ layerQuad.borderColor = 'rgba(154,205,50,0.75)';
+ } else {
+ layerQuad.borderColor = 'rgba(0,0,0,0.75)';
+ }
+ layerQuad.stackingGroupId = nextStackingGroupId++;
+ layerQuad.selectionToSetIfClicked = new cc.LayerSelection(layer);
+ layerQuad.layer = layer;
+ if (this.showOtherLayers && this.selectedLayer === layer) {
+ layerQuad.upperBorderColor = 'rgb(156,189,45)';
+ }
+
+ if (this.showAnimationBounds) {
+ this.appendAnimationQuads_(quads, layer, layerQuad);
+ }
+
+ this.appendImageQuads_(quads, layer, layerQuad);
+ quads.push(layerQuad);
+
+
+ if (this.showInvalidations) {
+ this.appendInvalidationQuads_(quads, layer, layerQuad);
+ }
+ if (this.showUnrecordedRegion) {
+ this.appendUnrecordedRegionQuads_(quads, layer, layerQuad);
+ }
+ if (this.showBottlenecks) {
+ this.appendBottleneckQuads_(quads, layer, layerQuad,
+ layerQuad.stackingGroupId);
+ }
+ if (this.showLayoutRects) {
+ this.appendLayoutRectQuads_(quads, layer, layerQuad);
+ }
+
+ if (this.howToShowTiles === 'coverage') {
+ this.appendTileCoverageRectQuads_(
+ quads, layer, layerQuad, this.tileHeatmapType);
+ } else if (this.howToShowTiles !== 'none') {
+ this.appendTilesWithScaleQuads_(
+ quads, layer, layerQuad,
+ this.howToShowTiles, this.tileHeatmapType);
+ }
+
+ let highlights;
+ highlights = extraHighlightsByLayerId[layer.layerId];
+ if (highlights) {
+ this.appendHighlightQuadsForLayer_(
+ quads, layer, layerQuad, highlights);
+ }
+
+ highlights = selectionHighlightsByLayerId[layer.layerId];
+ if (highlights) {
+ this.appendHighlightQuadsForLayer_(
+ quads, layer, layerQuad, highlights);
+ }
+ }
+
+ this.layerTreeImpl.iterLayers(function(layer, depth, isMask, isReplica) {
+ if (!this.showOtherLayers && this.selectedLayer !== layer) return;
+ if (alreadyVisitedLayerIds[layer.layerId]) return;
+
+ const layerQuad = layer.layerQuad;
+ const stackingGroupId = nextStackingGroupId++;
+ if (this.showBottlenecks) {
+ this.appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId);
+ }
+ }, this);
+
+ const tracedInputLatencies = this.layerTreeImpl.tracedInputLatencies;
+ if (this.showInputEvents && tracedInputLatencies) {
+ for (let i = 0; i < tracedInputLatencies.length; i++) {
+ const coordinatesArray =
+ tracedInputLatencies[i].args.data.coordinates;
+ for (let j = 0; j < coordinatesArray.length; j++) {
+ const inputQuad = tr.b.math.Quad.fromXYWH(
+ coordinatesArray[j].x - 25,
+ coordinatesArray[j].y - 25,
+ 50,
+ 50);
+ inputQuad.borderColor = 'rgba(0, 0, 0, 0)';
+ inputQuad.imageData = this.inputEventImageData_;
+ quads.push(inputQuad);
+ }
+ }
+ }
+
+ return quads;
+ },
+
+ updateInfoBar_(infoBarMessages) {
+ if (infoBarMessages.length) {
+ this.infoBar_.removeAllButtons();
+ this.infoBar_.message = 'Some problems were encountered...';
+ this.infoBar_.addButton('More info...', function(e) {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = '';
+ infoBarMessages.forEach(function(message) {
+ const title = document.createElement('h3');
+ Polymer.dom(title).textContent = message.header;
+
+ const details = document.createElement('div');
+ Polymer.dom(details).textContent = message.details;
+
+ Polymer.dom(overlay).appendChild(title);
+ Polymer.dom(overlay).appendChild(details);
+ });
+ overlay.visible = true;
+
+ e.stopPropagation();
+ return false;
+ });
+ this.infoBar_.visible = true;
+ } else {
+ this.infoBar_.removeAllButtons();
+ this.infoBar_.message = '';
+ this.infoBar_.visible = false;
+ }
+ },
+
+ getWhatRasterized_() {
+ const lthi = this.layerTreeImpl_.layerTreeHostImpl;
+ const renderProcess = lthi.objectInstance.parent;
+ const tasks = [];
+ for (const event of renderProcess.getDescendantEvents()) {
+ if (!(event instanceof tr.model.Slice)) continue;
+
+ const tile = tr.e.cc.getTileFromRasterTaskSlice(event);
+ if (tile === undefined) continue;
+
+ if (tile.containingSnapshot === lthi) {
+ tasks.push(event);
+ }
+ }
+ return tasks;
+ },
+
+ updateWhatRasterizedLinkState_() {
+ const tasks = this.getWhatRasterized_();
+ if (tasks.length) {
+ Polymer.dom(this.whatRasterizedLink_).textContent =
+ tasks.length + ' raster tasks';
+ this.whatRasterizedLink_.style.display = '';
+ } else {
+ Polymer.dom(this.whatRasterizedLink_).textContent = '';
+ this.whatRasterizedLink_.style.display = 'none';
+ }
+ },
+
+ getWhatRasterizedEventSet_() {
+ return new tr.model.EventSet(this.getWhatRasterized_());
+ }
+ };
+
+ return {
+ LayerTreeQuadStackView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html
new file mode 100644
index 00000000000..66932ae785f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('tileCoverageRectCount', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+
+ const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView();
+ view.layerTreeImpl = lthi.activeTree;
+ view.howToShowTiles = 'none';
+ view.showInvalidations = false;
+ view.showContents = false;
+
+ // There should be some quads drawn with all "show" checkboxes off,
+ // but that number can change with new features added.
+ const aQuads = view.generateLayerQuads();
+ view.howToShowTiles = 'coverage';
+ const bQuads = view.generateLayerQuads();
+ const numCoverageRects = bQuads.length - aQuads.length;
+
+ // We know we have 5 coverage rects in lthi cats.
+ assert.strictEqual(numCoverageRects, 5);
+ });
+
+ test('inputEvent', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+ lthi.activeTree.tracedInputLatencies =
+ [{args: {data: {coordinates: [{x: 10, y: 20}, {x: 30, y: 40}]}}}];
+
+ const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView();
+ view.layerTreeImpl = lthi.activeTree;
+ view.showInputEvents = false;
+
+ const aQuads = view.generateLayerQuads();
+ view.showInputEvents = true;
+ const bQuads = view.generateLayerQuads();
+ const numInputEventRects = bQuads.length - aQuads.length;
+
+ assert.strictEqual(numInputEventRects, 2);
+
+ // We should not start loading the image until the view is added into the
+ // DOM tree.
+ const image = Polymer.dom(view).querySelector('#input-event');
+ assert.strictEqual(getComputedStyle(image).backgroundImage, '');
+ assert.strictEqual(image.src, '');
+
+ document.body.appendChild(view);
+ view.updateContents_();
+ assert.notEqual(getComputedStyle(image).backgroundImage, '');
+ assert.notEqual(image.src, '');
+ view.remove();
+ });
+
+ test('invalidation', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+
+ const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView();
+ view.layerTreeImpl = lthi.activeTree;
+ view.showInvalidations = false;
+
+ const aQuads = view.generateLayerQuads();
+ view.showInvalidations = true;
+ const bQuads = view.generateLayerQuads();
+ const numInvalidationRects = bQuads.length - aQuads.length;
+
+ // We know we have 3 invalidation rects.
+ assert.strictEqual(numInvalidationRects, 3);
+
+ const expectedRectTypes = [
+ 'Invalidation rect (appeared) for client1',
+ 'Invalidation rect (disappeared) for client2',
+ 'Invalidation rect' // The non-annotated rect.
+ ];
+ const found = [];
+ for (const quad of bQuads) {
+ const i = expectedRectTypes.indexOf(quad.selectionToSetIfClicked &&
+ quad.selectionToSetIfClicked.rectType_);
+ if (i !== -1) {
+ found[i] = true;
+ }
+ }
+ assert.deepEqual(found, [true, true, true]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html
new file mode 100644
index 00000000000..56ecf770ec1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview LayerView coordinates graphical and analysis views of layers.
+ */
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const constants = tr.e.cc.constants;
+
+ /**
+ * @constructor
+ */
+ const LayerView = tr.ui.b.define('tr-ui-e-chrome-cc-layer-view');
+
+ LayerView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.flexDirection = 'column';
+ this.style.display = 'flex';
+
+ this.layerTreeQuadStackView_ =
+ new tr.ui.e.chrome.cc.LayerTreeQuadStackView();
+ this.dragBar_ = document.createElement('tr-ui-b-drag-handle');
+ this.analysisEl_ =
+ document.createElement('tr-ui-e-chrome-cc-layer-view-analysis');
+ this.analysisEl_.style.flexGrow = 0;
+ this.analysisEl_.style.flexShrink = 0;
+ this.analysisEl_.style.flexBasis = 'auto';
+ this.analysisEl_.style.height = '150px';
+ this.analysisEl_.style.overflow = 'auto';
+ this.analysisEl_.addEventListener('requestSelectionChange',
+ this.onRequestSelectionChangeFromAnalysisEl_.bind(this));
+
+ this.dragBar_.target = this.analysisEl_;
+
+ Polymer.dom(this).appendChild(this.layerTreeQuadStackView_);
+ Polymer.dom(this).appendChild(this.dragBar_);
+ Polymer.dom(this).appendChild(this.analysisEl_);
+
+ this.layerTreeQuadStackView_.addEventListener('selection-change',
+ function() {
+ this.layerTreeQuadStackViewSelectionChanged_();
+ }.bind(this));
+ this.layerTreeQuadStackViewSelectionChanged_();
+ },
+
+ get layerTreeImpl() {
+ return this.layerTreeQuadStackView_.layerTreeImpl;
+ },
+
+ set layerTreeImpl(newValue) {
+ return this.layerTreeQuadStackView_.layerTreeImpl = newValue;
+ },
+
+ set isRenderPassQuads(newValue) {
+ return this.layerTreeQuadStackView_.isRenderPassQuads = newValue;
+ },
+
+ get selection() {
+ return this.layerTreeQuadStackView_.selection;
+ },
+
+ set selection(newValue) {
+ this.layerTreeQuadStackView_.selection = newValue;
+ },
+
+ regenerateContent() {
+ this.layerTreeQuadStackView_.regenerateContent();
+ },
+
+ layerTreeQuadStackViewSelectionChanged_() {
+ const selection = this.layerTreeQuadStackView_.selection;
+ if (selection) {
+ this.dragBar_.style.display = '';
+ this.analysisEl_.style.display = '';
+ Polymer.dom(this.analysisEl_).textContent = '';
+
+ const layer = selection.layer;
+ if (tr.e.cc.PictureSnapshot.CanDebugPicture() &&
+ layer &&
+ layer.args &&
+ layer.args.pictures &&
+ layer.args.pictures.length) {
+ Polymer.dom(this.analysisEl_).appendChild(
+ this.createPictureBtn_(layer.args.pictures));
+ }
+
+ const analysis = selection.createAnalysis();
+ Polymer.dom(this.analysisEl_).appendChild(analysis);
+ for (const child of this.analysisEl_.children) {
+ child.style.userSelect = 'text';
+ }
+ } else {
+ this.dragBar_.style.display = 'none';
+ this.analysisEl_.style.display = 'none';
+ const analysis = Polymer.dom(this.analysisEl_).firstChild;
+ if (analysis) {
+ Polymer.dom(this.analysisEl_).removeChild(analysis);
+ }
+ this.layerTreeQuadStackView_.style.height =
+ window.getComputedStyle(this).height;
+ }
+ tr.b.dispatchSimpleEvent(this, 'selection-change');
+ },
+
+ createPictureBtn_(pictures) {
+ if (!(pictures instanceof Array)) {
+ pictures = [pictures];
+ }
+
+ const link = document.createElement('tr-ui-a-analysis-link');
+ link.selection = function() {
+ const layeredPicture = new tr.e.cc.LayeredPicture(pictures);
+ const snapshot = new tr.e.cc.PictureSnapshot(layeredPicture);
+ snapshot.picture = layeredPicture;
+
+ const selection = new tr.model.EventSet();
+ selection.push(snapshot);
+ return selection;
+ };
+ Polymer.dom(link).textContent = 'View in Picture Debugger';
+ return link;
+ },
+
+ onRequestSelectionChangeFromAnalysisEl_(e) {
+ if (!(e.selection instanceof tr.ui.e.chrome.cc.Selection)) {
+ return;
+ }
+
+ e.stopPropagation();
+ this.selection = e.selection;
+ },
+
+ get extraHighlightsByLayerId() {
+ return this.layerTreeQuadStackView_.extraHighlightsByLayerId;
+ },
+
+ set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+ this.layerTreeQuadStackView_.extraHighlightsByLayerId =
+ extraHighlightsByLayerId;
+ }
+ };
+
+ return {
+ LayerView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html
new file mode 100644
index 00000000000..ed3de7b87e1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+ const numLayers = lthi.activeTree.renderSurfaceLayerList.length;
+ const layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1];
+
+ const view = new tr.ui.e.chrome.cc.LayerView();
+ view.style.height = '500px';
+ view.layerTreeImpl = lthi.activeTree;
+ view.selection = new tr.ui.e.chrome.cc.LayerSelection(layer);
+
+ this.addHTMLOutput(view);
+ });
+
+ test('instantiate_withTileHighlight', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+
+ const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+ const lthi = instance.snapshots[0];
+ const numLayers = lthi.activeTree.renderSurfaceLayerList.length;
+ const layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1];
+ const tile = lthi.activeTiles[0];
+
+ const view = new tr.ui.e.chrome.cc.LayerView();
+ view.style.height = '500px';
+ view.layerTreeImpl = lthi.activeTree;
+ view.selection = new tr.ui.e.chrome.cc.TileSelection(tile);
+ this.addHTMLOutput(view);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html
new file mode 100644
index 00000000000..5dc62b1b4f6
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html
@@ -0,0 +1,455 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base64.html">
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/info_bar.html">
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import"
+ href="/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html">
+
+<template id="tr-ui-e-chrome-cc-picture-debugger-template">
+ <left-panel>
+ <picture-info>
+ <div>
+ <span class='title'>Skia Picture</span>
+ <span class='size'></span>
+ </div>
+ <div>
+ <input class='filename' type='text' value='skpicture.skp' />
+ <button class='export'>Export</button>
+ </div>
+ </picture-info>
+ </left-panel>
+ <right-panel>
+ <tr-ui-e-chrome-cc-picture-ops-chart-view>
+ </tr-ui-e-chrome-cc-picture-ops-chart-view>
+ <raster-area><canvas></canvas></raster-area>
+ </right-panel>
+</template>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const THIS_DOC = document.currentScript.ownerDocument;
+
+ /**
+ * PictureDebugger is a view of a PictureSnapshot for inspecting
+ * the picture in detail. (e.g., timing information, etc.)
+ *
+ * @constructor
+ */
+ const PictureDebugger = tr.ui.b.define('tr-ui-e-chrome-cc-picture-debugger');
+
+ PictureDebugger.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ const node = tr.ui.b.instantiateTemplate(
+ '#tr-ui-e-chrome-cc-picture-debugger-template', THIS_DOC);
+
+ Polymer.dom(this).appendChild(node);
+
+ this.style.display = 'flex';
+ this.style.flexDirection = 'row';
+
+ const title = this.querySelector('.title');
+ title.style.fontWeight = 'bold';
+ title.style.marginLeft = '5px';
+ title.style.marginRight = '5px';
+
+ this.pictureAsImageData_ = undefined;
+ this.showOverdraw_ = false;
+ this.zoomScaleValue_ = 1;
+
+ this.sizeInfo_ = Polymer.dom(this).querySelector('.size');
+ this.rasterArea_ = Polymer.dom(this).querySelector('raster-area');
+ this.rasterArea_.style.backgroundColor = '#ddd';
+ this.rasterArea_.style.minHeight = '100px';
+ this.rasterArea_.style.minWidth = '200px';
+ this.rasterArea_.style.overflow = 'auto';
+ this.rasterArea_.style.paddingLeft = '5px';
+ this.rasterCanvas_ = Polymer.dom(this.rasterArea_)
+ .querySelector('canvas');
+ this.rasterCtx_ = this.rasterCanvas_.getContext('2d');
+
+ this.filename_ = Polymer.dom(this).querySelector('.filename');
+ this.filename_.style.userSelect = 'text';
+ this.filename_.style.marginLeft = '5px';
+
+ this.drawOpsChartSummaryView_ =
+ new tr.ui.e.chrome.cc.PictureOpsChartSummaryView();
+ this.drawOpsChartView_ = new tr.ui.e.chrome.cc.PictureOpsChartView();
+ this.drawOpsChartView_.addEventListener(
+ 'selection-changed', this.onChartBarClicked_.bind(this));
+
+ this.exportButton_ = Polymer.dom(this).querySelector('.export');
+ this.exportButton_.addEventListener(
+ 'click', this.onSaveAsSkPictureClicked_.bind(this));
+
+ this.trackMouse_();
+
+ const overdrawCheckbox = tr.ui.b.createCheckBox(
+ this, 'showOverdraw',
+ 'pictureView.showOverdraw', false,
+ 'Show overdraw');
+
+ const chartCheckbox = tr.ui.b.createCheckBox(
+ this, 'showSummaryChart',
+ 'pictureView.showSummaryChart', false,
+ 'Show timing summary');
+
+ const pictureInfo = Polymer.dom(this).querySelector('picture-info');
+ pictureInfo.style.flexGrow = 0;
+ pictureInfo.style.flexShrink = 0;
+ pictureInfo.style.flexBasis = 'auto';
+ pictureInfo.style.paddingTop = '2px';
+ Polymer.dom(pictureInfo).appendChild(overdrawCheckbox);
+ Polymer.dom(pictureInfo).appendChild(chartCheckbox);
+
+ this.drawOpsView_ = new tr.ui.e.chrome.cc.PictureOpsListView();
+ this.drawOpsView_.flexGrow = 1;
+ this.drawOpsView_.flexShrink = 1;
+ this.drawOpsView_.flexBasis = 'auto';
+ this.drawOpsView_.addEventListener(
+ 'selection-changed', this.onChangeDrawOps_.bind(this));
+
+ const leftPanel = Polymer.dom(this).querySelector('left-panel');
+ leftPanel.style.flexDirection = 'column';
+ leftPanel.style.display = 'flex';
+ leftPanel.style.flexGrow = 0;
+ leftPanel.style.flexShrink = 0;
+ leftPanel.style.flexBasis = 'auto';
+ leftPanel.style.minWidth = '200px';
+ leftPanel.style.overflow = 'auto';
+ Polymer.dom(leftPanel).appendChild(this.drawOpsChartSummaryView_);
+ Polymer.dom(leftPanel).appendChild(this.drawOpsView_);
+
+ const middleDragHandle = document.createElement('tr-ui-b-drag-handle');
+ middleDragHandle.style.flexGrow = 0;
+ middleDragHandle.style.flexShrink = 0;
+ middleDragHandle.style.flexBasis = 'auto';
+ middleDragHandle.horizontal = false;
+ middleDragHandle.target = leftPanel;
+
+ const rightPanel = Polymer.dom(this).querySelector('right-panel');
+ rightPanel.style.flexGrow = 1;
+ rightPanel.style.flexShrink = 1;
+ rightPanel.style.flexBasis = 'auto';
+ rightPanel.style.minWidth = 0;
+ rightPanel.style.flexDirection = 'column';
+ rightPanel.style.display = 'flex';
+
+ const chartView = Polymer.dom(rightPanel).querySelector(
+ 'tr-ui-e-chrome-cc-picture-ops-chart-view');
+ this.drawOpsChartView_.style.flexGrow = 0;
+ this.drawOpsChartView_.style.flexShrink = 0;
+ this.drawOpsChartView_.style.flexBasis = 'auto';
+ this.drawOpsChartView_.style.minWidth = 0;
+ this.drawOpsChartView_.style.overflowX = 'auto';
+ this.drawOpsChartView_.style.overflowY = 'hidden';
+ rightPanel.replaceChild(this.drawOpsChartView_, chartView);
+
+ this.infoBar_ = document.createElement('tr-ui-b-info-bar');
+ Polymer.dom(this.rasterArea_).appendChild(this.infoBar_);
+
+ Polymer.dom(this).insertBefore(middleDragHandle, rightPanel);
+
+ this.picture_ = undefined;
+
+ const hkc = document.createElement('tv-ui-b-hotkey-controller');
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ thisArg: this,
+ keyCode: 'h'.charCodeAt(0),
+ callback(e) {
+ this.moveSelectedOpBy(-1);
+ e.stopPropagation();
+ }
+ }));
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ thisArg: this,
+ keyCode: 'l'.charCodeAt(0),
+ callback(e) {
+ this.moveSelectedOpBy(1);
+ e.stopPropagation();
+ }
+ }));
+ Polymer.dom(this).appendChild(hkc);
+ },
+
+ onSaveAsSkPictureClicked_() {
+ // Decode base64 data into a String
+ const rawData = tr.b.Base64.atob(this.picture_.getBase64SkpData());
+
+ // Convert this String into an Uint8Array
+ const length = rawData.length;
+ const arrayBuffer = new ArrayBuffer(length);
+ const uint8Array = new Uint8Array(arrayBuffer);
+ for (let c = 0; c < length; c++) {
+ uint8Array[c] = rawData.charCodeAt(c);
+ }
+
+ // Create a blob URL from the binary array.
+ const blob = new Blob([uint8Array], {type: 'application/octet-binary'});
+ const blobUrl = window.webkitURL.createObjectURL(blob);
+
+ // Create a link and click on it. BEST API EVAR!
+ const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+ link.href = blobUrl;
+ link.download = this.filename_.value;
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(
+ 'click', true, false, window, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null);
+ link.dispatchEvent(event);
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set picture(picture) {
+ this.drawOpsView_.picture = picture;
+ this.drawOpsChartView_.picture = picture;
+ this.drawOpsChartSummaryView_.picture = picture;
+ this.picture_ = picture;
+
+ this.exportButton_.disabled = !this.picture_.canSave;
+
+ if (picture) {
+ const size = this.getRasterCanvasSize_();
+ this.rasterCanvas_.width = size.width;
+ this.rasterCanvas_.height = size.height;
+ }
+
+ const bounds = this.rasterArea_.getBoundingClientRect();
+ const selectorBounds = this.mouseModeSelector_.getBoundingClientRect();
+ this.mouseModeSelector_.pos = {
+ x: (bounds.right - selectorBounds.width - 10),
+ y: bounds.top
+ };
+
+ this.rasterize_();
+
+ this.scheduleUpdateContents_();
+ },
+
+ getRasterCanvasSize_() {
+ const style = window.getComputedStyle(this.rasterArea_);
+ const width =
+ Math.max(parseInt(style.width), this.picture_.layerRect.width);
+ const height =
+ Math.max(parseInt(style.height), this.picture_.layerRect.height);
+
+ return {
+ width,
+ height
+ };
+ },
+
+ scheduleUpdateContents_() {
+ if (this.updateContentsPending_) return;
+
+ this.updateContentsPending_ = true;
+ tr.b.requestAnimationFrameInThisFrameIfPossible(
+ this.updateContents_.bind(this)
+ );
+ },
+
+ updateContents_() {
+ this.updateContentsPending_ = false;
+
+ if (this.picture_) {
+ Polymer.dom(this.sizeInfo_).textContent = '(' +
+ this.picture_.layerRect.width + ' x ' +
+ this.picture_.layerRect.height + ')';
+ }
+
+ this.drawOpsChartView_.updateChartContents();
+ this.drawOpsChartView_.scrollSelectedItemIntoViewIfNecessary();
+
+ // Return if picture hasn't finished rasterizing.
+ if (!this.pictureAsImageData_) return;
+
+ this.infoBar_.visible = false;
+ this.infoBar_.removeAllButtons();
+ if (this.pictureAsImageData_.error) {
+ this.infoBar_.message = 'Cannot rasterize...';
+ this.infoBar_.addButton('More info...', function(e) {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = this.pictureAsImageData_.error;
+ overlay.visible = true;
+ e.stopPropagation();
+ return false;
+ }.bind(this));
+ this.infoBar_.visible = true;
+ }
+
+ this.drawPicture_();
+ },
+
+ drawPicture_() {
+ const size = this.getRasterCanvasSize_();
+ if (size.width !== this.rasterCanvas_.width) {
+ this.rasterCanvas_.width = size.width;
+ }
+ if (size.height !== this.rasterCanvas_.height) {
+ this.rasterCanvas_.height = size.height;
+ }
+
+ this.rasterCtx_.clearRect(0, 0, size.width, size.height);
+
+ if (!this.pictureAsImageData_.imageData) return;
+
+ const imgCanvas = this.pictureAsImageData_.asCanvas();
+ const w = imgCanvas.width;
+ const h = imgCanvas.height;
+ this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h,
+ 0, 0, w * this.zoomScaleValue_,
+ h * this.zoomScaleValue_);
+ },
+
+ rasterize_() {
+ if (this.picture_) {
+ this.picture_.rasterize(
+ {
+ stopIndex: this.drawOpsView_.selectedOpIndex,
+ showOverdraw: this.showOverdraw_
+ },
+ this.onRasterComplete_.bind(this));
+ }
+ },
+
+ onRasterComplete_(pictureAsImageData) {
+ this.pictureAsImageData_ = pictureAsImageData;
+ this.scheduleUpdateContents_();
+ },
+
+ moveSelectedOpBy(increment) {
+ if (this.selectedOpIndex === undefined) {
+ this.selectedOpIndex = 0;
+ return;
+ }
+ this.selectedOpIndex = tr.b.math.clamp(
+ this.selectedOpIndex + increment,
+ 0, this.numOps);
+ },
+
+ get numOps() {
+ return this.drawOpsView_.numOps;
+ },
+
+ get selectedOpIndex() {
+ return this.drawOpsView_.selectedOpIndex;
+ },
+
+ set selectedOpIndex(index) {
+ this.drawOpsView_.selectedOpIndex = index;
+ this.drawOpsChartView_.selectedOpIndex = index;
+ },
+
+ onChartBarClicked_(e) {
+ this.drawOpsView_.selectedOpIndex =
+ this.drawOpsChartView_.selectedOpIndex;
+ },
+
+ onChangeDrawOps_(e) {
+ this.rasterize_();
+ this.scheduleUpdateContents_();
+
+ this.drawOpsChartView_.selectedOpIndex =
+ this.drawOpsView_.selectedOpIndex;
+ },
+
+ set showOverdraw(v) {
+ this.showOverdraw_ = v;
+ this.rasterize_();
+ },
+
+ set showSummaryChart(chartShouldBeVisible) {
+ if (chartShouldBeVisible) {
+ this.drawOpsChartSummaryView_.show();
+ } else {
+ this.drawOpsChartSummaryView_.hide();
+ }
+ },
+
+ trackMouse_() {
+ this.mouseModeSelector_ = document.createElement(
+ 'tr-ui-b-mouse-mode-selector');
+ this.mouseModeSelector_.targetElement = this.rasterArea_;
+ Polymer.dom(this.rasterArea_).appendChild(this.mouseModeSelector_);
+
+ this.mouseModeSelector_.supportedModeMask =
+ tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.defaultMode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM;
+ this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector';
+
+ this.mouseModeSelector_.addEventListener('beginzoom',
+ this.onBeginZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('updatezoom',
+ this.onUpdateZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('endzoom',
+ this.onEndZoom_.bind(this));
+ },
+
+ onBeginZoom_(e) {
+ this.isZooming_ = true;
+
+ this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+
+ e.preventDefault();
+ },
+
+ onUpdateZoom_(e) {
+ if (!this.isZooming_) return;
+
+ const currentMouseViewPos = this.extractRelativeMousePosition_(e);
+
+ // Take the distance the mouse has moved and we want to zoom at about
+ // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned
+ // more if people feel it's too slow.
+ this.zoomScaleValue_ +=
+ ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001);
+ this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1);
+
+ this.drawPicture_();
+
+ this.lastMouseViewPos_ = currentMouseViewPos;
+ },
+
+ onEndZoom_(e) {
+ this.lastMouseViewPos_ = undefined;
+ this.isZooming_ = false;
+ e.preventDefault();
+ },
+
+ extractRelativeMousePosition_(e) {
+ return {
+ x: e.clientX - this.rasterArea_.offsetLeft,
+ y: e.clientY - this.rasterArea_.offsetTop
+ };
+ }
+ };
+
+ return {
+ PictureDebugger,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html
new file mode 100644
index 00000000000..e89e6b355e1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const picture = new tr.e.cc.PictureSnapshot({id: '31415'}, 10, {
+ 'params': {
+ 'opaque_rect': [-15, -15, 0, 0],
+ 'layer_rect': [-15, -15, 46, 833]
+ },
+ 'skp64': 'DAAAAHYEAADzAQAABwAAAAFkYWVy8AAAAAgAAB4DAAAADAAAIAAAgD8AAIA/CAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADoAAAACAAAHgMAAAAMAAAjAAAAAAAAAAAMAAAjAAAAAAAAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADkAAAAGAAAFQEAAAAAAAAAAAAAAADAjkQAgPlDGAAAFQIAAAAAAAAAAAAAAADAjkQAgPlDCAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADgAAAAGAAAFQMAAAAAAKBAAACgQAAAgEIAAIBCBAAAHAQAABwEAAAcBAAAHHRjYWYBAAAADVNrU3JjWGZlcm1vZGVjZnB0AAAAAHlhcmGgAAAAIHRucAMAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgEAAAP//ADABAAAAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgED/////AjABAAAAAAAAAAAAAAAAAAEAAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEEAAIA/AAAAAAAAgD8AAAAAAACAQP8AAP8AMAEAAAAAACBmb2U=' // @suppress longLineCheck
+ });
+ picture.preInitialize();
+ picture.initialize();
+
+ const dbg = new tr.ui.e.chrome.cc.PictureDebugger();
+ this.addHTMLOutput(dbg);
+ dbg.picture = picture;
+ dbg.style.border = '1px solid black';
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html
new file mode 100644
index 00000000000..55a8685aee0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html
@@ -0,0 +1,458 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const OPS_TIMING_ITERATIONS = 3;
+ const CHART_PADDING_LEFT = 65;
+ const CHART_PADDING_RIGHT = 40;
+ const AXIS_PADDING_LEFT = 60;
+ const AXIS_PADDING_RIGHT = 35;
+ const AXIS_PADDING_TOP = 25;
+ const AXIS_PADDING_BOTTOM = 45;
+ const AXIS_LABEL_PADDING = 5;
+ const AXIS_TICK_SIZE = 10;
+ const LABEL_PADDING = 5;
+ const LABEL_INTERLEAVE_OFFSET = 15;
+ const BAR_PADDING = 5;
+ const VERTICAL_TICKS = 5;
+ const HUE_CHAR_CODE_ADJUSTMENT = 5.7;
+
+ /**
+ * Provides a chart showing the cumulative time spent in Skia operations
+ * during picture rasterization.
+ *
+ * @constructor
+ */
+ const PictureOpsChartSummaryView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-picture-ops-chart-summary-view');
+
+ PictureOpsChartSummaryView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.flexGrow = 0;
+ this.style.flexShrink = 0;
+ this.style.flexBasis = 'auto';
+ this.style.fontSize = 0;
+ this.style.margin = 0;
+ this.style.minHeight = '200px';
+ this.style.minWidth = '200px';
+ this.style.overflow = 'hidden';
+ this.style.padding = 0;
+
+ this.picture_ = undefined;
+ this.pictureDataProcessed_ = false;
+
+ this.chartScale_ = window.devicePixelRatio;
+
+ this.chart_ = document.createElement('canvas');
+ this.chartCtx_ = this.chart_.getContext('2d');
+ Polymer.dom(this).appendChild(this.chart_);
+
+ this.opsTimingData_ = [];
+
+ this.chartWidth_ = 0;
+ this.chartHeight_ = 0;
+ this.requiresRedraw_ = true;
+
+ this.currentBarMouseOverTarget_ = null;
+
+ this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
+ new ResizeObserver(this.onResize_.bind(this)).observe(this);
+ },
+
+ get requiresRedraw() {
+ return this.requiresRedraw_;
+ },
+
+ set requiresRedraw(requiresRedraw) {
+ this.requiresRedraw_ = requiresRedraw;
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+ this.pictureDataProcessed_ = false;
+
+ if (Polymer.dom(this).classList.contains('hidden')) return;
+
+ this.processPictureData_();
+ this.requiresRedraw = true;
+ this.updateChartContents();
+ },
+
+ hide() {
+ Polymer.dom(this).classList.add('hidden');
+ this.style.display = 'none';
+ },
+
+ show() {
+ Polymer.dom(this).classList.remove('hidden');
+ this.style.display = '';
+
+ if (!this.pictureDataProcessed_) {
+ this.processPictureData_();
+ }
+ this.requiresRedraw = true;
+ this.updateChartContents();
+ },
+
+ onMouseMove_(e) {
+ const lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
+ this.currentBarMouseOverTarget_ = null;
+
+ const x = e.offsetX;
+ const y = e.offsetY;
+
+ const chartLeft = CHART_PADDING_LEFT;
+ const chartRight = this.chartWidth_ - CHART_PADDING_RIGHT;
+ const chartTop = AXIS_PADDING_TOP;
+ const chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM;
+ const chartInnerWidth = chartRight - chartLeft;
+
+ if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) {
+ this.currentBarMouseOverTarget_ = Math.floor(
+ (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length);
+
+ this.currentBarMouseOverTarget_ = tr.b.math.clamp(
+ this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1);
+ }
+
+ if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) return;
+
+ this.drawChartContents_();
+ },
+
+ onResize_() {
+ this.requiresRedraw = true;
+ this.updateChartContents();
+ },
+
+ updateChartContents() {
+ if (this.requiresRedraw) {
+ this.updateChartDimensions_();
+ }
+
+ this.drawChartContents_();
+ },
+
+ updateChartDimensions_() {
+ this.chartWidth_ = this.offsetWidth;
+ this.chartHeight_ = this.offsetHeight;
+
+ // Scale up the canvas according to the devicePixelRatio, then reduce it
+ // down again via CSS. Finally we apply a scale to the canvas so that
+ // things are drawn at the correct size.
+ this.chart_.width = this.chartWidth_ * this.chartScale_;
+ this.chart_.height = this.chartHeight_ * this.chartScale_;
+
+ this.chart_.style.width = this.chartWidth_ + 'px';
+ this.chart_.style.height = this.chartHeight_ + 'px';
+
+ this.chartCtx_.scale(this.chartScale_, this.chartScale_);
+ },
+
+ processPictureData_() {
+ this.resetOpsTimingData_();
+ this.pictureDataProcessed_ = true;
+
+ if (!this.picture_) return;
+
+ let ops = this.picture_.getOps();
+ if (!ops) return;
+
+ ops = this.picture_.tagOpsWithTimings(ops);
+
+ // Check that there are valid times.
+ if (ops[0].cmd_time === undefined) return;
+
+ this.collapseOpsToTimingBuckets_(ops);
+ },
+
+ drawChartContents_() {
+ this.clearChartContents_();
+
+ if (this.opsTimingData_.length === 0) {
+ this.showNoTimingDataMessage_();
+ return;
+ }
+
+ this.drawChartAxes_();
+ this.drawBars_();
+ this.drawLineAtBottomOfChart_();
+
+ if (this.currentBarMouseOverTarget_ === null) return;
+
+ this.drawTooltip_();
+ },
+
+ drawLineAtBottomOfChart_() {
+ this.chartCtx_.strokeStyle = '#AAA';
+ this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
+ this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
+ this.chartCtx_.stroke();
+ },
+
+ drawTooltip_() {
+ const tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_];
+ const tooltipTitle = tooltipData.cmd_string;
+ const tooltipTime = tooltipData.cmd_time.toFixed(4);
+
+ const tooltipWidth = 110;
+ const tooltipHeight = 40;
+ const chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
+ CHART_PADDING_LEFT;
+ const barWidth = chartInnerWidth / this.opsTimingData_.length;
+ const tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
+
+ const left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
+ barWidth - tooltipOffset;
+ const top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.shadowOffsetX = 0;
+ this.chartCtx_.shadowOffsetY = 5;
+ this.chartCtx_.shadowBlur = 4;
+ this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
+
+ this.chartCtx_.strokeStyle = '#888';
+ this.chartCtx_.fillStyle = '#EEE';
+ this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
+
+ this.chartCtx_.shadowColor = 'transparent';
+ this.chartCtx_.translate(0.5, 0.5);
+ this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
+
+ this.chartCtx_.restore();
+
+ this.chartCtx_.fillStyle = '#222';
+ this.chartCtx_.textBaseline = 'top';
+ this.chartCtx_.font = '800 12px Arial';
+ this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
+
+ this.chartCtx_.fillStyle = '#555';
+ this.chartCtx_.textBaseline = 'top';
+ this.chartCtx_.font = '400 italic 10px Arial';
+ this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms',
+ left + 8, top + 22);
+ },
+
+ drawBars_() {
+ const len = this.opsTimingData_.length;
+ const max = this.opsTimingData_[0].cmd_time;
+ const min = this.opsTimingData_[len - 1].cmd_time;
+
+ const width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT;
+ const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+ const barWidth = Math.floor(width / len);
+
+ let opData;
+ let opTiming;
+ let opHeight;
+ let opLabel;
+ let barLeft;
+
+ for (let b = 0; b < len; b++) {
+ opData = this.opsTimingData_[b];
+ opTiming = opData.cmd_time / max;
+
+ opHeight = Math.round(Math.max(1, opTiming * height));
+ opLabel = opData.cmd_string;
+ barLeft = CHART_PADDING_LEFT + b * barWidth;
+
+ this.chartCtx_.fillStyle = this.getOpColor_(opLabel);
+
+ this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP +
+ height - opHeight, barWidth - 2 * BAR_PADDING, opHeight);
+ }
+ },
+
+ getOpColor_(opName) {
+ const characters = opName.split('');
+ const hue = characters.reduce(this.reduceNameToHue, 0) % 360;
+
+ return 'hsl(' + hue + ', 30%, 50%)';
+ },
+
+ reduceNameToHue(previousValue, currentValue, index, array) {
+ // Get the char code and apply a magic adjustment value so we get
+ // pretty colors from around the rainbow.
+ return Math.round(previousValue + currentValue.charCodeAt(0) *
+ HUE_CHAR_CODE_ADJUSTMENT);
+ },
+
+ drawChartAxes_() {
+ const len = this.opsTimingData_.length;
+ const max = this.opsTimingData_[0].cmd_time;
+ const min = this.opsTimingData_[len - 1].cmd_time;
+
+ const width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
+ const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+
+ const totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT -
+ CHART_PADDING_RIGHT;
+ const barWidth = Math.floor(totalBarWidth / len);
+ const tickYInterval = height / (VERTICAL_TICKS - 1);
+ let tickYPosition = 0;
+ const tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
+ let tickVal = 0;
+
+ this.chartCtx_.fillStyle = '#333';
+ this.chartCtx_.strokeStyle = '#777';
+ this.chartCtx_.save();
+
+ // Translate half a pixel to avoid blurry lines.
+ this.chartCtx_.translate(0.5, 0.5);
+
+ // Sides.
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+ this.chartCtx_.moveTo(0, 0);
+ this.chartCtx_.lineTo(0, height);
+ this.chartCtx_.lineTo(width, height);
+
+ // Y-axis ticks.
+ this.chartCtx_.font = '10px Arial';
+ this.chartCtx_.textAlign = 'right';
+ this.chartCtx_.textBaseline = 'middle';
+
+ for (let t = 0; t < VERTICAL_TICKS; t++) {
+ tickYPosition = Math.round(t * tickYInterval);
+ tickVal = (max - t * tickValInterval).toFixed(4);
+
+ this.chartCtx_.moveTo(0, tickYPosition);
+ this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
+ this.chartCtx_.fillText(tickVal,
+ -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
+ }
+
+ this.chartCtx_.stroke();
+
+ this.chartCtx_.restore();
+
+
+ // Labels.
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5),
+ AXIS_PADDING_TOP + height + LABEL_PADDING);
+
+ this.chartCtx_.font = '10px Arial';
+ this.chartCtx_.textAlign = 'center';
+ this.chartCtx_.textBaseline = 'top';
+
+ let labelTickLeft;
+ let labelTickBottom;
+ for (let l = 0; l < len; l++) {
+ labelTickLeft = Math.round(l * barWidth);
+ labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET;
+
+ this.chartCtx_.save();
+ this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING);
+ this.chartCtx_.lineTo(labelTickLeft, labelTickBottom);
+ this.chartCtx_.stroke();
+ this.chartCtx_.restore();
+
+ this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string,
+ labelTickLeft, labelTickBottom);
+ }
+
+ this.chartCtx_.restore();
+
+ this.chartCtx_.restore();
+ },
+
+ clearChartContents_() {
+ this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
+ },
+
+ showNoTimingDataMessage_() {
+ this.chartCtx_.font = '800 italic 14px Arial';
+ this.chartCtx_.fillStyle = '#333';
+ this.chartCtx_.textAlign = 'center';
+ this.chartCtx_.textBaseline = 'middle';
+ this.chartCtx_.fillText('No timing data available.',
+ this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
+ },
+
+ collapseOpsToTimingBuckets_(ops) {
+ const opsTimingDataIndexHash_ = {};
+ const timingData = this.opsTimingData_;
+ let op;
+ let opIndex;
+
+ for (let i = 0; i < ops.length; i++) {
+ op = ops[i];
+
+ if (op.cmd_time === undefined) continue;
+
+ // Try to locate the entry for the current operation
+ // based on its name. If that fails, then create one for it.
+ opIndex = opsTimingDataIndexHash_[op.cmd_string] || null;
+
+ if (opIndex === null) {
+ timingData.push({
+ cmd_time: 0,
+ cmd_string: op.cmd_string
+ });
+
+ opIndex = timingData.length - 1;
+ opsTimingDataIndexHash_[op.cmd_string] = opIndex;
+ }
+
+ timingData[opIndex].cmd_time += op.cmd_time;
+ }
+
+ timingData.sort(this.sortTimingBucketsByOpTimeDescending_);
+
+ this.collapseTimingBucketsToOther_(4);
+ },
+
+ collapseTimingBucketsToOther_(count) {
+ const timingData = this.opsTimingData_;
+ const otherSource = timingData.splice(count, timingData.length - count);
+ let otherDestination = null;
+
+ if (!otherSource.length) return;
+
+ timingData.push({
+ cmd_time: 0,
+ cmd_string: 'Other'
+ });
+
+ otherDestination = timingData[timingData.length - 1];
+ for (let i = 0; i < otherSource.length; i++) {
+ otherDestination.cmd_time += otherSource[i].cmd_time;
+ }
+ },
+
+ sortTimingBucketsByOpTimeDescending_(a, b) {
+ return b.cmd_time - a.cmd_time;
+ },
+
+ resetOpsTimingData_() {
+ this.opsTimingData_.length = 0;
+ }
+ };
+
+ return {
+ PictureOpsChartSummaryView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html
new file mode 100644
index 00000000000..413998847aa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html
@@ -0,0 +1,505 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const BAR_PADDING = 1;
+ const BAR_WIDTH = 5;
+ const CHART_PADDING_LEFT = 65;
+ const CHART_PADDING_RIGHT = 30;
+ const CHART_PADDING_BOTTOM = 35;
+ const CHART_PADDING_TOP = 20;
+ const AXIS_PADDING_LEFT = 55;
+ const AXIS_PADDING_RIGHT = 30;
+ const AXIS_PADDING_BOTTOM = 35;
+ const AXIS_PADDING_TOP = 20;
+ const AXIS_TICK_SIZE = 5;
+ const AXIS_LABEL_PADDING = 5;
+ const VERTICAL_TICKS = 5;
+ const HUE_CHAR_CODE_ADJUSTMENT = 5.7;
+
+ /**
+ * Provides a chart showing the cumulative time spent in Skia operations
+ * during picture rasterization.
+ *
+ * @constructor
+ */
+ const PictureOpsChartView =
+ tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-chart-view');
+
+ PictureOpsChartView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.display = 'block';
+ this.style.height = '180px';
+ this.style.margin = 0;
+ this.style.padding = 0;
+ this.style.position = 'relative';
+
+ this.picture_ = undefined;
+ this.pictureOps_ = undefined;
+ this.opCosts_ = undefined;
+
+ this.chartScale_ = window.devicePixelRatio;
+
+ this.chart_ = document.createElement('canvas');
+ this.chartCtx_ = this.chart_.getContext('2d');
+ Polymer.dom(this).appendChild(this.chart_);
+
+ this.selectedOpIndex_ = undefined;
+ this.chartWidth_ = 0;
+ this.chartHeight_ = 0;
+ this.dimensionsHaveChanged_ = true;
+
+ this.currentBarMouseOverTarget_ = undefined;
+
+ this.ninetyFifthPercentileCost_ = 0;
+ this.totalOpCost_ = 0;
+
+ this.chart_.addEventListener('click', this.onClick_.bind(this));
+ this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
+ new ResizeObserver(this.onResize_.bind(this)).observe(this);
+
+ this.usePercentileScale_ = false;
+ this.usePercentileScaleCheckbox_ = tr.ui.b.createCheckBox(
+ this, 'usePercentileScale',
+ 'PictureOpsChartView.usePercentileScale', false,
+ 'Limit to 95%-ile');
+ Polymer.dom(this.usePercentileScaleCheckbox_).classList.add(
+ 'use-percentile-scale');
+ this.usePercentileScaleCheckbox_.style.position = 'absolute';
+ this.usePercentileScaleCheckbox_.style.left = 0;
+ this.usePercentileScaleCheckbox_.style.top = 0;
+ Polymer.dom(this).appendChild(this.usePercentileScaleCheckbox_);
+ },
+
+ get dimensionsHaveChanged() {
+ return this.dimensionsHaveChanged_;
+ },
+
+ set dimensionsHaveChanged(dimensionsHaveChanged) {
+ this.dimensionsHaveChanged_ = dimensionsHaveChanged;
+ },
+
+ get usePercentileScale() {
+ return this.usePercentileScale_;
+ },
+
+ set usePercentileScale(usePercentileScale) {
+ this.usePercentileScale_ = usePercentileScale;
+ this.drawChartContents_();
+ },
+
+ get numOps() {
+ return this.opCosts_.length;
+ },
+
+ get selectedOpIndex() {
+ return this.selectedOpIndex_;
+ },
+
+ set selectedOpIndex(selectedOpIndex) {
+ if (selectedOpIndex < 0) throw new Error('Invalid index');
+ if (selectedOpIndex >= this.numOps) throw new Error('Invalid index');
+
+ this.selectedOpIndex_ = selectedOpIndex;
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+ this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps());
+ this.currentBarMouseOverTarget_ = undefined;
+ this.processPictureData_();
+ this.dimensionsHaveChanged = true;
+ },
+
+ processPictureData_() {
+ if (this.pictureOps_ === undefined) return;
+
+ let totalOpCost = 0;
+
+ // Take a copy of the picture ops data for sorting.
+ this.opCosts_ = this.pictureOps_.map(function(op) {
+ totalOpCost += op.cmd_time;
+ return op.cmd_time;
+ });
+ this.opCosts_.sort();
+
+ const ninetyFifthPercentileCostIndex = Math.floor(
+ this.opCosts_.length * 0.95);
+ this.ninetyFifthPercentileCost_ =
+ this.opCosts_[ninetyFifthPercentileCostIndex];
+ this.maxCost_ = this.opCosts_[this.opCosts_.length - 1];
+
+ this.totalOpCost_ = totalOpCost;
+ },
+
+ extractBarIndex_(e) {
+ let index = undefined;
+
+ if (this.pictureOps_ === undefined ||
+ this.pictureOps_.length === 0) {
+ return index;
+ }
+
+ const x = e.offsetX;
+ const y = e.offsetY;
+
+ const totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
+
+ const chartLeft = CHART_PADDING_LEFT;
+ const chartTop = 0;
+ const chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
+ const chartRight = chartLeft + totalBarWidth;
+
+ if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom) {
+ return index;
+ }
+
+ index = Math.floor((x - chartLeft) / totalBarWidth *
+ this.pictureOps_.length);
+
+ index = tr.b.math.clamp(index, 0, this.pictureOps_.length - 1);
+
+ return index;
+ },
+
+ onClick_(e) {
+ const barClicked = this.extractBarIndex_(e);
+
+ if (barClicked === undefined) return;
+
+ // If we click on the already selected item we should deselect.
+ if (barClicked === this.selectedOpIndex) {
+ this.selectedOpIndex = undefined;
+ } else {
+ this.selectedOpIndex = barClicked;
+ }
+
+ e.preventDefault();
+
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ },
+
+ onMouseMove_(e) {
+ const lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
+ this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
+
+ if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) {
+ return;
+ }
+
+ this.drawChartContents_();
+ },
+
+ onResize_() {
+ this.dimensionsHaveChanged = true;
+ this.updateChartContents();
+ },
+
+ scrollSelectedItemIntoViewIfNecessary() {
+ if (this.selectedOpIndex === undefined) {
+ return;
+ }
+
+ const width = this.offsetWidth;
+ const left = this.scrollLeft;
+ const right = left + width;
+ const targetLeft = CHART_PADDING_LEFT +
+ (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
+
+ if (targetLeft > left && targetLeft < right) {
+ return;
+ }
+
+ this.scrollLeft = (targetLeft - width * 0.5);
+ },
+
+ updateChartContents() {
+ if (this.dimensionsHaveChanged) {
+ this.updateChartDimensions_();
+ }
+
+ this.drawChartContents_();
+ },
+
+ updateChartDimensions_() {
+ if (!this.pictureOps_) return;
+
+ let width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
+ ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
+
+ if (width < this.offsetWidth) {
+ width = this.offsetWidth;
+ }
+
+ // Allow the element to be its natural size as set by flexbox, then lock
+ // the width in before we set the width of the canvas.
+ this.chartWidth_ = width;
+ this.chartHeight_ = this.getBoundingClientRect().height;
+
+ // Scale up the canvas according to the devicePixelRatio, then reduce it
+ // down again via CSS. Finally we apply a scale to the canvas so that
+ // things are drawn at the correct size.
+ this.chart_.width = this.chartWidth_ * this.chartScale_;
+ this.chart_.height = this.chartHeight_ * this.chartScale_;
+
+ this.chart_.style.width = this.chartWidth_ + 'px';
+ this.chart_.style.height = this.chartHeight_ + 'px';
+
+ this.chartCtx_.scale(this.chartScale_, this.chartScale_);
+
+ this.dimensionsHaveChanged = false;
+ },
+
+ drawChartContents_() {
+ this.clearChartContents_();
+
+ if (this.pictureOps_ === undefined ||
+ this.pictureOps_.length === 0 ||
+ this.pictureOps_[0].cmd_time === undefined) {
+ this.showNoTimingDataMessage_();
+ return;
+ }
+
+ this.drawSelection_();
+ this.drawBars_();
+ this.drawChartAxes_();
+ this.drawLinesAtTickMarks_();
+ this.drawLineAtBottomOfChart_();
+
+ if (this.currentBarMouseOverTarget_ === undefined) {
+ return;
+ }
+
+ this.drawTooltip_();
+ },
+
+ drawSelection_() {
+ if (this.selectedOpIndex === undefined) {
+ return;
+ }
+
+ const width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
+ this.chartCtx_.fillStyle = 'rgb(223, 235, 230)';
+ this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP,
+ width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM);
+ },
+
+ drawChartAxes_() {
+ const min = this.opCosts_[0];
+ const max = this.opCosts_[this.opCosts_.length - 1];
+ const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+
+ const tickYInterval = height / (VERTICAL_TICKS - 1);
+ let tickYPosition = 0;
+ const tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
+ let tickVal = 0;
+
+ this.chartCtx_.fillStyle = '#333';
+ this.chartCtx_.strokeStyle = '#777';
+ this.chartCtx_.save();
+
+ // Translate half a pixel to avoid blurry lines.
+ this.chartCtx_.translate(0.5, 0.5);
+
+ // Sides.
+ this.chartCtx_.beginPath();
+ this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+ this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ -
+ AXIS_PADDING_BOTTOM);
+ this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT,
+ this.chartHeight_ - AXIS_PADDING_BOTTOM);
+ this.chartCtx_.stroke();
+ this.chartCtx_.closePath();
+
+ // Y-axis ticks.
+ this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+
+ this.chartCtx_.font = '10px Arial';
+ this.chartCtx_.textAlign = 'right';
+ this.chartCtx_.textBaseline = 'middle';
+
+ this.chartCtx_.beginPath();
+ for (let t = 0; t < VERTICAL_TICKS; t++) {
+ tickYPosition = Math.round(t * tickYInterval);
+ tickVal = (max - t * tickValInterval).toFixed(4);
+
+ this.chartCtx_.moveTo(0, tickYPosition);
+ this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
+ this.chartCtx_.fillText(tickVal,
+ -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
+ }
+
+ this.chartCtx_.stroke();
+ this.chartCtx_.closePath();
+
+ this.chartCtx_.restore();
+ },
+
+ drawLinesAtTickMarks_() {
+ const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+ const width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
+ const tickYInterval = height / (VERTICAL_TICKS - 1);
+ let tickYPosition = 0;
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5);
+ this.chartCtx_.beginPath();
+ this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)';
+
+ for (let t = 0; t < VERTICAL_TICKS; t++) {
+ tickYPosition = Math.round(t * tickYInterval);
+
+ this.chartCtx_.moveTo(0, tickYPosition);
+ this.chartCtx_.lineTo(width, tickYPosition);
+ this.chartCtx_.stroke();
+ }
+
+ this.chartCtx_.restore();
+ this.chartCtx_.closePath();
+ },
+
+ drawLineAtBottomOfChart_() {
+ this.chartCtx_.strokeStyle = '#AAA';
+ this.chartCtx_.beginPath();
+ this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
+ this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
+ this.chartCtx_.stroke();
+ this.chartCtx_.closePath();
+ },
+
+ drawTooltip_() {
+ const tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_];
+ const tooltipTitle = tooltipData.cmd_string;
+ const tooltipTime = tooltipData.cmd_time.toFixed(4);
+ const toolTipTimePercentage =
+ ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2);
+
+ const tooltipWidth = 120;
+ const tooltipHeight = 40;
+ const chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
+ CHART_PADDING_LEFT;
+ const barWidth = BAR_WIDTH + BAR_PADDING;
+ const tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
+
+ const left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
+ barWidth - tooltipOffset;
+ const top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
+
+ this.chartCtx_.save();
+
+ this.chartCtx_.shadowOffsetX = 0;
+ this.chartCtx_.shadowOffsetY = 5;
+ this.chartCtx_.shadowBlur = 4;
+ this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
+
+ this.chartCtx_.strokeStyle = '#888';
+ this.chartCtx_.fillStyle = '#EEE';
+ this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
+
+ this.chartCtx_.shadowColor = 'transparent';
+ this.chartCtx_.translate(0.5, 0.5);
+ this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
+
+ this.chartCtx_.restore();
+
+ this.chartCtx_.fillStyle = '#222';
+ this.chartCtx_.textAlign = 'left';
+ this.chartCtx_.textBaseline = 'top';
+ this.chartCtx_.font = '800 12px Arial';
+ this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
+
+ this.chartCtx_.fillStyle = '#555';
+ this.chartCtx_.font = '400 italic 10px Arial';
+ this.chartCtx_.fillText(tooltipTime + 'ms (' +
+ toolTipTimePercentage + '%)', left + 8, top + 22);
+ },
+
+ drawBars_() {
+ let op;
+ let opColor = 0;
+ let opHeight = 0;
+ const opWidth = BAR_WIDTH + BAR_PADDING;
+ let opHover = false;
+
+ const bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
+ const maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
+ CHART_PADDING_TOP;
+
+ let maxValue;
+ if (this.usePercentileScale) {
+ maxValue = this.ninetyFifthPercentileCost_;
+ } else {
+ maxValue = this.maxCost_;
+ }
+
+ for (let b = 0; b < this.pictureOps_.length; b++) {
+ op = this.pictureOps_[b];
+ opHeight = Math.round(
+ (op.cmd_time / maxValue) * maxHeight);
+ opHeight = Math.max(opHeight, 1);
+ opHover = (b === this.currentBarMouseOverTarget_);
+ opColor = this.getOpColor_(op.cmd_string, opHover);
+
+ if (b === this.selectedOpIndex) {
+ this.chartCtx_.fillStyle = '#FFFF00';
+ } else {
+ this.chartCtx_.fillStyle = opColor;
+ }
+
+ this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
+ bottom - opHeight, BAR_WIDTH, opHeight);
+ }
+ },
+
+ getOpColor_(opName, hover) {
+ const characters = opName.split('');
+
+ const hue = characters.reduce(this.reduceNameToHue, 0) % 360;
+ const saturation = 30;
+ const lightness = hover ? '75%' : '50%';
+
+ return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
+ },
+
+ reduceNameToHue(previousValue, currentValue, index, array) {
+ // Get the char code and apply a magic adjustment value so we get
+ // pretty colors from around the rainbow.
+ return Math.round(previousValue + currentValue.charCodeAt(0) *
+ HUE_CHAR_CODE_ADJUSTMENT);
+ },
+
+ clearChartContents_() {
+ this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
+ },
+
+ showNoTimingDataMessage_() {
+ this.chartCtx_.font = '800 italic 14px Arial';
+ this.chartCtx_.fillStyle = '#333';
+ this.chartCtx_.textAlign = 'center';
+ this.chartCtx_.textBaseline = 'middle';
+ this.chartCtx_.fillText('No timing data available.',
+ this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
+ }
+ };
+
+ return {
+ PictureOpsChartView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html
new file mode 100644
index 00000000000..2e45be58c33
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html
@@ -0,0 +1,261 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/constants.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/list_view.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ const OPS_TIMING_ITERATIONS = 3; // Iterations to average op timing info over.
+ const ANNOTATION = 'Comment';
+ const BEGIN_ANNOTATION = 'BeginCommentGroup';
+ const END_ANNOTATION = 'EndCommentGroup';
+ const ANNOTATION_ID = 'ID: ';
+ const ANNOTATION_CLASS = 'CLASS: ';
+ const ANNOTATION_TAG = 'TAG: ';
+
+ const constants = tr.e.cc.constants;
+
+ /**
+ * @constructor
+ */
+ const PictureOpsListView =
+ tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-list-view');
+
+ PictureOpsListView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate() {
+ this.style.borderTop = '1px solid grey';
+ this.style.overflow = 'auto';
+ this.opsList_ = new tr.ui.b.ListView();
+ Polymer.dom(this).appendChild(this.opsList_);
+
+ this.selectedOp_ = undefined;
+ this.selectedOpIndex_ = undefined;
+ this.opsList_.addEventListener(
+ 'selection-changed', this.onSelectionChanged_.bind(this));
+
+ this.picture_ = undefined;
+ },
+
+ get picture() {
+ return this.picture_;
+ },
+
+ set picture(picture) {
+ this.picture_ = picture;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.opsList_.clear();
+
+ if (!this.picture_) return;
+
+ let ops = this.picture_.getOps();
+ if (!ops) return;
+
+ ops = this.picture_.tagOpsWithTimings(ops);
+
+ ops = this.opsTaggedWithAnnotations_(ops);
+
+ for (let i = 0; i < ops.length; i++) {
+ const op = ops[i];
+ const item = document.createElement('div');
+ item.opIndex = op.opIndex;
+ Polymer.dom(item).textContent = i + ') ' + op.cmd_string;
+
+ // Display the element info associated with the op, if available.
+ if (op.elementInfo.tag || op.elementInfo.id || op.elementInfo.class) {
+ const elementInfo = document.createElement('span');
+ Polymer.dom(elementInfo).classList.add('elementInfo');
+ elementInfo.style.color = 'purple';
+ elementInfo.style.fontSize = 'small';
+ elementInfo.style.fontWeight = 'bold';
+ elementInfo.style.color = '#777';
+ const tag = op.elementInfo.tag ? op.elementInfo.tag : 'unknown';
+ const id = op.elementInfo.id ? 'id=' + op.elementInfo.id : undefined;
+ const className = op.elementInfo.class ? 'class=' +
+ op.elementInfo.class : undefined;
+ Polymer.dom(elementInfo).textContent =
+ '<' + tag + (id ? ' ' : '') +
+ (id ? id : '') + (className ? ' ' : '') +
+ (className ? className : '') + '>';
+ Polymer.dom(item).appendChild(elementInfo);
+ }
+
+ // Display the Skia params.
+ // FIXME: now that we have structured data, we should format it.
+ // (https://github.com/google/trace-viewer/issues/782)
+ if (op.info.length > 0) {
+ const infoItem = document.createElement('div');
+ Polymer.dom(infoItem).textContent = JSON.stringify(op.info);
+ infoItem.style.fontSize = 'x-small';
+ infoItem.style.color = '#777';
+ Polymer.dom(item).appendChild(infoItem);
+ }
+
+ // Display the op timing, if available.
+ if (op.cmd_time && op.cmd_time >= 0.0001) {
+ const time = document.createElement('span');
+ Polymer.dom(time).classList.add('time');
+ const rounded = op.cmd_time.toFixed(4);
+ Polymer.dom(time).textContent = '(' + rounded + 'ms)';
+ time.style.fontSize = 'x-small';
+ time.style.color = 'rgb(136, 0, 0)';
+ Polymer.dom(item).appendChild(time);
+ }
+
+ item.style.borderBottom = '1px solid #555';
+ item.style.fontSize = 'small';
+ item.style.fontWeight = 'bold';
+ item.style.paddingBottom = '5px';
+ item.style.paddingLeft = '5px';
+ item.style.cursor = 'pointer';
+
+ for (const child of item.children) {
+ child.style.fontWeight = 'normal';
+ child.style.marginLeft = '1em';
+ child.style.maxWidth = '300px';
+ }
+
+ Polymer.dom(this.opsList_).appendChild(item);
+ }
+ },
+
+ onSelectionChanged_(e) {
+ let beforeSelectedOp = true;
+
+ // Deselect on re-selection.
+ if (this.opsList_.selectedElement === this.selectedOp_) {
+ this.opsList_.selectedElement = undefined;
+ beforeSelectedOp = false;
+ this.selectedOpIndex_ = undefined;
+ }
+
+ this.selectedOp_ = this.opsList_.selectedElement;
+
+ // Set selection on all previous ops.
+ const ops = this.opsList_.children;
+ for (let i = 0; i < ops.length; i++) {
+ const op = ops[i];
+ if (op === this.selectedOp_) {
+ beforeSelectedOp = false;
+ this.selectedOpIndex_ = op.opIndex;
+ } else if (beforeSelectedOp) {
+ Polymer.dom(op).setAttribute('beforeSelection', 'beforeSelection');
+ op.style.backgroundColor = 'rgb(103, 199, 165)';
+ } else {
+ Polymer.dom(op).removeAttribute('beforeSelection');
+ op.style.backgroundColor = '';
+ }
+ }
+
+ tr.b.dispatchSimpleEvent(this, 'selection-changed', false);
+ },
+
+ get numOps() {
+ return this.opsList_.children.length;
+ },
+
+ get selectedOpIndex() {
+ return this.selectedOpIndex_;
+ },
+
+ set selectedOpIndex(s) {
+ this.selectedOpIndex_ = s;
+
+ if (s === undefined) {
+ this.opsList_.selectedElement = this.selectedOp_;
+ this.onSelectionChanged_();
+ } else {
+ if (s < 0) throw new Error('Invalid index');
+ if (s >= this.numOps) throw new Error('Invalid index');
+ this.opsList_.selectedElement = this.opsList_.getElementByIndex(s + 1);
+ tr.ui.b.scrollIntoViewIfNeeded(this.opsList_.selectedElement);
+ }
+ },
+
+ /**
+ * Return Skia operations tagged by annotation.
+ *
+ * The ops returned from Picture.getOps() contain both Skia ops and
+ * annotations threaded together. This function removes all annotations
+ * from the list and tags each op with the associated annotations.
+ * Additionally, the last {tag, id, class} is stored as elementInfo on
+ * each op.
+ *
+ * @param {Array} ops Array of Skia operations and annotations.
+ * @return {Array} Skia ops where op.annotations contains the associated
+ * annotations for a given op.
+ */
+ opsTaggedWithAnnotations_(ops) {
+ // This algorithm works by walking all the ops and pushing any
+ // annotations onto a stack. When a non-annotation op is found, the
+ // annotations stack is traversed and stored with the op.
+ const annotationGroups = [];
+ const opsWithoutAnnotations = [];
+ for (let opIndex = 0; opIndex < ops.length; opIndex++) {
+ const op = ops[opIndex];
+ op.opIndex = opIndex;
+ switch (op.cmd_string) {
+ case BEGIN_ANNOTATION:
+ annotationGroups.push([]);
+ break;
+ case END_ANNOTATION:
+ annotationGroups.pop();
+ break;
+ case ANNOTATION:
+ annotationGroups[annotationGroups.length - 1].push(op);
+ break;
+ default: {
+ const annotations = [];
+ let elementInfo = {};
+ annotationGroups.forEach(function(annotationGroup) {
+ elementInfo = {};
+ annotationGroup.forEach(function(annotation) {
+ annotation.info.forEach(function(info) {
+ if (info.includes(ANNOTATION_TAG)) {
+ elementInfo.tag = info.substring(
+ info.indexOf(ANNOTATION_TAG) +
+ ANNOTATION_TAG.length).toLowerCase();
+ } else if (info.includes(ANNOTATION_ID)) {
+ elementInfo.id = info.substring(
+ info.indexOf(ANNOTATION_ID) +
+ ANNOTATION_ID.length);
+ } else if (info.includes(ANNOTATION_CLASS)) {
+ elementInfo.class = info.substring(
+ info.indexOf(ANNOTATION_CLASS) +
+ ANNOTATION_CLASS.length);
+ }
+
+ annotations.push(info);
+ });
+ });
+ });
+ op.annotations = annotations;
+ op.elementInfo = elementInfo;
+ opsWithoutAnnotations.push(op);
+ }
+ }
+ }
+
+ return opsWithoutAnnotations;
+ }
+ };
+
+ return {
+ PictureOpsListView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html
new file mode 100644
index 00000000000..b58c1568f4f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const PictureOpsListView = tr.ui.e.chrome.cc.PictureOpsListView;
+
+ test('instantiate', function() {
+ if (!tr.e.cc.PictureSnapshot.CanRasterize()) return;
+
+ const m = new tr.Model(g_catLTHIEvents);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+ const snapshot = instance.snapshots[0];
+
+ const view = new PictureOpsListView();
+ view.picture = snapshot;
+ assert.strictEqual(view.opsList_.children.length, 142);
+ });
+
+ test('selection', function() {
+ if (!tr.e.cc.PictureSnapshot.CanRasterize()) return;
+
+ const m = new tr.Model(g_catLTHIEvents);
+ const p = Object.values(m.processes)[0];
+
+ const instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+ const snapshot = instance.snapshots[0];
+
+ const view = new PictureOpsListView();
+ view.picture = snapshot;
+ let didSelectionChange = 0;
+ view.addEventListener('selection-changed', function() {
+ didSelectionChange = true;
+ });
+ assert.isFalse(didSelectionChange);
+ view.opsList_.selectedElement = view.opsList_.children[3];
+ assert.isTrue(didSelectionChange);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html
new file mode 100644
index 00000000000..a9db575773f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/picture.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /*
+ * Displays a picture snapshot in a human readable form.
+ * @constructor
+ */
+ const PictureSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-picture-snapshot-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ PictureSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add(
+ 'tr-ui-e-chrome-cc-picture-snapshot-view');
+ this.style.display = 'flex';
+ this.style.flexGrow = 1;
+ this.style.flexShrink = 1;
+ this.style.flexBasis = 'auto';
+ this.style.minWidth = 0;
+ this.pictureDebugger_ = new tr.ui.e.chrome.cc.PictureDebugger();
+ this.pictureDebugger_.style.flexGrow = 1;
+ this.pictureDebugger_.style.flexShrink = 1;
+ this.pictureDebugger_.style.flexBasis = 'auto';
+ this.pictureDebugger_.style.minWidth = 0;
+ Polymer.dom(this).appendChild(this.pictureDebugger_);
+ },
+
+ updateContents() {
+ if (this.objectSnapshot_ && this.pictureDebugger_) {
+ this.pictureDebugger_.picture = this.objectSnapshot_;
+ }
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ PictureSnapshotView,
+ {
+ typeNames: ['cc::Picture', 'cc::LayeredPicture'],
+ showInstances: false
+ });
+
+ return {
+ PictureSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html
new file mode 100644
index 00000000000..6b1a7cb7df0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /**
+ * @constructor
+ */
+ function RasterTaskSelection(selection) {
+ tr.ui.e.chrome.cc.Selection.call(this);
+ const whySupported = RasterTaskSelection.whySuported(selection);
+ if (!whySupported.ok) {
+ throw new Error('Fail: ' + whySupported.why);
+ }
+ this.slices_ = Array.from(selection);
+ this.tiles_ = this.slices_.map(function(slice) {
+ const tile = tr.e.cc.getTileFromRasterTaskSlice(slice);
+ if (tile === undefined) {
+ throw new Error('This should never happen due to .supports check.');
+ }
+ return tile;
+ });
+ }
+
+ RasterTaskSelection.whySuported = function(selection) {
+ if (!(selection instanceof tr.model.EventSet)) {
+ return {ok: false, why: 'Must be selection'};
+ }
+
+ if (selection.length === 0) {
+ return {ok: false, why: 'Selection must be non empty'};
+ }
+
+ let referenceSnapshot = undefined;
+ for (const event of selection) {
+ if (!(event instanceof tr.model.Slice)) {
+ return {ok: false, why: 'Not a slice'};
+ }
+
+ const tile = tr.e.cc.getTileFromRasterTaskSlice(event);
+ if (tile === undefined) {
+ return {ok: false, why: 'No tile found'};
+ }
+
+ if (!referenceSnapshot) {
+ referenceSnapshot = tile.containingSnapshot;
+ } else {
+ if (tile.containingSnapshot !== referenceSnapshot) {
+ return {
+ ok: false,
+ why: 'Raster tasks are from different compositor instances'
+ };
+ }
+ }
+ }
+ return {ok: true};
+ };
+
+ RasterTaskSelection.supports = function(selection) {
+ return RasterTaskSelection.whySuported(selection).ok;
+ };
+
+ RasterTaskSelection.prototype = {
+ __proto__: tr.ui.e.chrome.cc.Selection.prototype,
+
+ get specicifity() {
+ return 3;
+ },
+
+ get associatedLayerId() {
+ const tile0 = this.tiles_[0];
+ const allSameLayer = this.tiles_.every(function(tile) {
+ tile.layerId === tile0.layerId;
+ });
+ if (allSameLayer) {
+ return tile0.layerId;
+ }
+ return undefined;
+ },
+
+ get extraHighlightsByLayerId() {
+ const highlights = {};
+ this.tiles_.forEach(function(tile, i) {
+ if (highlights[tile.layerId] === undefined) {
+ highlights[tile.layerId] = [];
+ }
+ const slice = this.slices_[i];
+ highlights[tile.layerId].push({
+ colorKey: slice.title,
+ rect: tile.layerRect
+ });
+ }, this);
+ return highlights;
+ },
+
+ createAnalysis() {
+ const sel = new tr.model.EventSet();
+ this.slices_.forEach(function(slice) {
+ sel.push(slice);
+ });
+
+ let analysis;
+ if (sel.length === 1) {
+ analysis = document.createElement('tr-ui-a-single-event-sub-view');
+ } else {
+ analysis = document.createElement('tr-ui-e-chrome-cc-raster-task-view');
+ }
+ analysis.selection = sel;
+ return analysis;
+ },
+
+ findEquivalent(lthi) {
+ // Raster tasks are only valid in one LTHI.
+ return undefined;
+ },
+
+ // RasterTaskSelection specific stuff follows.
+ get containingSnapshot() {
+ return this.tiles_[0].containingSnapshot;
+ }
+ };
+
+ return {
+ RasterTaskSelection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html
new file mode 100644
index 00000000000..d95a7135d37
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+ const rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) {
+ return slice.title === 'RasterTask';
+ });
+
+ let selection = new tr.model.EventSet();
+ selection.push(rasterTasks[0]);
+ selection.push(rasterTasks[1]);
+
+ assert.isTrue(tr.ui.e.chrome.cc.RasterTaskSelection.supports(selection));
+ selection = new tr.ui.e.chrome.cc.RasterTaskSelection(selection);
+ const highlights = selection.extraHighlightsByLayerId;
+ assert.lengthOf(Object.keys(highlights), 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html
new file mode 100644
index 00000000000..a5f7f5d806c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-e-chrome-cc-raster-task-view'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ #heading {
+ flex: 0 0 auto;
+ }
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+
+ <div id="heading">
+ Rasterization costs in
+ <tr-ui-a-analysis-link id="link"></tr-ui-a-analysis-link>
+ </div>
+ <tr-ui-b-table id="content"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-chrome-cc-raster-task-view',
+
+ created() {
+ this.selection_ = undefined;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+
+ this.updateContents_();
+ },
+
+ updateColumns_(hadCpuDurations) {
+ const timeSpanConfig = {
+ unit: tr.b.Unit.byName.timeDurationInMs,
+ ownerDocument: this.ownerDocument
+ };
+
+ const columns = [
+ {
+ title: 'Layer',
+ value(row) {
+ if (row.isTotals) return 'Totals';
+ if (row.layer) {
+ const linkEl = document.createElement('tr-ui-a-analysis-link');
+ linkEl.setSelectionAndContent(
+ function() {
+ return new tr.ui.e.chrome.cc.LayerSelection(row.layer);
+ },
+ 'Layer ' + row.layerId);
+ return linkEl;
+ }
+ return 'Layer ' + row.layerId;
+ },
+ width: '250px'
+ },
+ {
+ title: 'Num Tiles',
+ value(row) { return row.numTiles; },
+ cmp(a, b) { return a.numTiles - b.numTiles; }
+ },
+ {
+ title: 'Num Analysis Tasks',
+ value(row) { return row.numAnalysisTasks; },
+ cmp(a, b) {
+ return a.numAnalysisTasks - b.numAnalysisTasks;
+ }
+ },
+ {
+ title: 'Num Raster Tasks',
+ value(row) { return row.numRasterTasks; },
+ cmp(a, b) { return a.numRasterTasks - b.numRasterTasks; }
+ },
+ {
+ title: 'Wall Duration (ms)',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.duration, timeSpanConfig);
+ },
+ cmp(a, b) { return a.duration - b.duration; }
+ }
+ ];
+
+ if (hadCpuDurations) {
+ columns.push({
+ title: 'CPU Duration (ms)',
+ value(row) {
+ return tr.v.ui.createScalarSpan(row.cpuDuration, timeSpanConfig);
+ },
+ cmp(a, b) { return a.cpuDuration - b.cpuDuration; }
+ });
+ }
+
+ let colWidthPercentage;
+ if (columns.length === 1) {
+ colWidthPercentage = '100%';
+ } else {
+ colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%';
+ }
+
+ for (let i = 1; i < columns.length; i++) {
+ columns[i].width = colWidthPercentage;
+ }
+
+ this.$.content.tableColumns = columns;
+ this.$.content.sortColumnIndex = columns.length - 1;
+ },
+
+ updateContents_() {
+ const table = this.$.content;
+
+ if (this.selection_.length === 0) {
+ this.$.link.setSelectionAndContent(undefined, '');
+ table.tableRows = [];
+ table.footerRows = [];
+ table.rebuild();
+ return;
+ }
+ // LTHI link.
+ const lthi = tr.e.cc.getTileFromRasterTaskSlice(
+ tr.b.getFirstElement(this.selection_)).containingSnapshot;
+ this.$.link.setSelectionAndContent(function() {
+ return new tr.model.EventSet(lthi);
+ }, lthi.userFriendlyName);
+
+ // Get costs by layer.
+ const costsByLayerId = {};
+ function getCurrentCostsForLayerId(tile) {
+ const layerId = tile.layerId;
+ const lthi = tile.containingSnapshot;
+ let layer;
+ if (lthi.activeTree) {
+ layer = lthi.activeTree.findLayerWithId(layerId);
+ }
+ if (layer === undefined && lthi.pendingTree) {
+ layer = lthi.pendingTree.findLayerWithId(layerId);
+ }
+ if (costsByLayerId[layerId] === undefined) {
+ costsByLayerId[layerId] = {
+ layerId,
+ layer,
+ numTiles: 0,
+ numAnalysisTasks: 0,
+ numRasterTasks: 0,
+ duration: 0,
+ cpuDuration: 0
+ };
+ }
+ return costsByLayerId[layerId];
+ }
+
+ let totalDuration = 0;
+ let totalCpuDuration = 0;
+ let totalNumAnalyzeTasks = 0;
+ let totalNumRasterizeTasks = 0;
+ let hadCpuDurations = false;
+
+ const tilesThatWeHaveSeen = {};
+
+ this.selection_.forEach(function(slice) {
+ const tile = tr.e.cc.getTileFromRasterTaskSlice(slice);
+ const curCosts = getCurrentCostsForLayerId(tile);
+
+ if (!tilesThatWeHaveSeen[tile.objectInstance.id]) {
+ tilesThatWeHaveSeen[tile.objectInstance.id] = true;
+ curCosts.numTiles += 1;
+ }
+
+ if (tr.e.cc.isSliceDoingAnalysis(slice)) {
+ curCosts.numAnalysisTasks += 1;
+ totalNumAnalyzeTasks += 1;
+ } else {
+ curCosts.numRasterTasks += 1;
+ totalNumRasterizeTasks += 1;
+ }
+ curCosts.duration += slice.duration;
+ totalDuration += slice.duration;
+ if (slice.cpuDuration !== undefined) {
+ curCosts.cpuDuration += slice.cpuDuration;
+ totalCpuDuration += slice.cpuDuration;
+ hadCpuDurations = true;
+ }
+ });
+
+ // Apply to the table.
+ this.updateColumns_(hadCpuDurations);
+ table.tableRows = Object.values(costsByLayerId);
+ table.rebuild();
+
+ // Footer.
+ table.footerRows = [
+ {
+ isTotals: true,
+ numTiles: Object.keys(tilesThatWeHaveSeen).length,
+ numAnalysisTasks: totalNumAnalyzeTasks,
+ numRasterTasks: totalNumRasterizeTasks,
+ duration: totalDuration,
+ cpuDuration: totalCpuDuration
+ }
+ ];
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html
new file mode 100644
index 00000000000..56767cfcc89
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html">
+
+<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js">
+</script>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createSelection() {
+ const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]);
+ const p = m.processes[1];
+ const rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) {
+ return slice.title === 'RasterTask' || slice.title === 'AnalyzeTask';
+ });
+
+ const selection = new tr.model.EventSet();
+ selection.model = m;
+
+ selection.push(rasterTasks[0]);
+ selection.push(rasterTasks[1]);
+ return selection;
+ }
+
+ test('basic', function() {
+ const selection = createSelection();
+ const view = document.createElement('tr-ui-e-chrome-cc-raster-task-view');
+ view.selection = selection;
+ this.addHTMLOutput(view);
+ });
+
+ test('analysisViewIntegration', function() {
+ const selection = createSelection();
+
+ const timelineView = {model: selection.model};
+ const brushingStateController =
+ new tr.c.BrushingStateController(timelineView);
+
+ const analysisEl = document.createElement('tr-ui-a-analysis-view');
+ analysisEl.brushingStateController = brushingStateController;
+ brushingStateController.changeSelectionFromTimeline(selection);
+
+ assert.isDefined(Polymer.dom(analysisEl).querySelector(
+ 'tr-ui-e-chrome-cc-raster-task-view'));
+
+ const sv = tr.ui.b.findDeepElementMatching(
+ analysisEl, 'tr-ui-a-multi-thread-slice-sub-view');
+ assert.isTrue(sv.requiresTallView);
+ this.addHTMLOutput(analysisEl);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html
new file mode 100644
index 00000000000..2794540e115
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html
@@ -0,0 +1,304 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ function Selection() {
+ this.selectionToSetIfClicked = undefined;
+ }
+ Selection.prototype = {
+ /**
+ * When two things are picked in the UI, one must occasionally tie-break
+ * between them to decide what was really clicked. Things with higher
+ * specicifity will win.
+ */
+ get specicifity() {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * If a selection is related to a specific layer, then this returns the
+ * layerId of that layer. If the selection is not related to a layer, for
+ * example if the device viewport is selected, then this returns undefined.
+ */
+ get associatedLayerId() {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * If a selection is related to a specific render pass, then this returns
+ * the layerId of that layer. If the selection is not related to a layer,
+ * for example if the device viewport is selected, then this returns
+ * undefined.
+ */
+ get associatedRenderPassId() {
+ throw new Error('Not implemented');
+ },
+
+
+ get highlightsByLayerId() {
+ return {};
+ },
+
+ /**
+ * Called when the selection is made active in the layer view. Must return
+ * an HTMLElement that explains this selection in detail.
+ */
+ createAnalysis() {
+ throw new Error('Not implemented');
+ },
+
+ /**
+ * Should try to create the equivalent selection in the provided LTHI,
+ * or undefined if it can't be done.
+ */
+ findEquivalent(lthi) {
+ throw new Error('Not implemented');
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function RenderPassSelection(renderPass, renderPassId) {
+ if (!renderPass || (renderPassId === undefined)) {
+ throw new Error('Render pass (with id) is required');
+ }
+ this.renderPass_ = renderPass;
+ this.renderPassId_ = renderPassId;
+ }
+
+ RenderPassSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 1;
+ },
+
+ get associatedLayerId() {
+ return undefined;
+ },
+
+ get associatedRenderPassId() {
+ return this.renderPassId_;
+ },
+
+ get renderPass() {
+ return this.renderPass_;
+ },
+
+ createAnalysis() {
+ const dataView = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ dataView.label = 'RenderPass ' + this.renderPassId_;
+ dataView.object = this.renderPass_.args;
+ return dataView;
+ },
+
+ get title() {
+ return this.renderPass_.objectInstance.typeName;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function LayerSelection(layer) {
+ if (!layer) {
+ throw new Error('Layer is required');
+ }
+ this.layer_ = layer;
+ }
+
+ LayerSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 1;
+ },
+
+ get associatedLayerId() {
+ return this.layer_.layerId;
+ },
+
+ get associatedRenderPassId() {
+ return undefined;
+ },
+
+ get layer() {
+ return this.layer_;
+ },
+
+ createAnalysis() {
+ const dataView = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ dataView.label = 'Layer ' + this.layer_.layerId;
+ if (this.layer_.usingGpuRasterization) {
+ dataView.label += ' (GPU-rasterized)';
+ }
+ dataView.object = this.layer_.args;
+ return dataView;
+ },
+
+ get title() {
+ return this.layer_.objectInstance.typeName;
+ },
+
+ findEquivalent(lthi) {
+ const layer = lthi.activeTree.findLayerWithId(this.layer_.layerId) ||
+ lthi.pendingTree.findLayerWithId(this.layer_.layerId);
+ if (!layer) return undefined;
+ return new LayerSelection(layer);
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function TileSelection(tile, opt_data) {
+ this.tile_ = tile;
+ this.data_ = opt_data || {};
+ }
+
+ TileSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 2;
+ },
+
+ get associatedLayerId() {
+ return this.tile_.layerId;
+ },
+
+ get highlightsByLayerId() {
+ const highlights = {};
+ highlights[this.tile_.layerId] = [
+ {
+ colorKey: this.tile_.objectInstance.typeName,
+ rect: this.tile_.layerRect
+ }
+ ];
+ return highlights;
+ },
+
+ createAnalysis() {
+ const analysis = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ analysis.label = 'Tile ' + this.tile_.objectInstance.id + ' on layer ' +
+ this.tile_.layerId;
+ if (this.data_) {
+ analysis.object = {
+ moreInfo: this.data_,
+ tileArgs: this.tile_.args
+ };
+ } else {
+ analysis.object = this.tile_.args;
+ }
+ return analysis;
+ },
+
+ findEquivalent(lthi) {
+ const tileInstance = this.tile_.tileInstance;
+ if (lthi.ts < tileInstance.creationTs ||
+ lthi.ts >= tileInstance.deletionTs) {
+ return undefined;
+ }
+ const tileSnapshot = tileInstance.getSnapshotAt(lthi.ts);
+ if (!tileSnapshot) return undefined;
+ return new TileSelection(tileSnapshot);
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function LayerRectSelection(layer, rectType, rect, opt_data) {
+ this.layer_ = layer;
+ this.rectType_ = rectType;
+ this.rect_ = rect;
+ this.data_ = opt_data !== undefined ? opt_data : rect;
+ }
+
+ LayerRectSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 2;
+ },
+
+ get associatedLayerId() {
+ return this.layer_.layerId;
+ },
+
+
+ get highlightsByLayerId() {
+ const highlights = {};
+ highlights[this.layer_.layerId] = [
+ {
+ colorKey: this.rectType_,
+ rect: this.rect_
+ }
+ ];
+ return highlights;
+ },
+
+ createAnalysis() {
+ const analysis = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ analysis.label = this.rectType_ + ' on layer ' + this.layer_.layerId;
+ analysis.object = this.data_;
+ return analysis;
+ },
+
+ findEquivalent(lthi) {
+ return undefined;
+ }
+ };
+
+ /**
+ * @constructor
+ */
+ function AnimationRectSelection(layer, rect) {
+ this.layer_ = layer;
+ this.rect_ = rect;
+ }
+
+ AnimationRectSelection.prototype = {
+ __proto__: Selection.prototype,
+
+ get specicifity() {
+ return 0;
+ },
+
+ get associatedLayerId() {
+ return this.layer_.layerId;
+ },
+
+ createAnalysis() {
+ const analysis = document.createElement(
+ 'tr-ui-a-generic-object-view-with-label');
+ analysis.label = 'Animation Bounds of layer ' + this.layer_.layerId;
+ analysis.object = this.rect_;
+ return analysis;
+ }
+ };
+
+ return {
+ Selection,
+ RenderPassSelection,
+ LayerSelection,
+ TileSelection,
+ LayerRectSelection,
+ AnimationRectSelection,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html
new file mode 100644
index 00000000000..ad1f633f334
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/cc/tile.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+
+<script>
+
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.cc', function() {
+ /*
+ * Displays a tile in a human readable form.
+ * @constructor
+ */
+ const TileSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-cc-tile-snapshot-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ TileSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add('tr-ui-e-chrome-cc-tile-snapshot-view');
+ this.layerTreeView_ =
+ new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView();
+ Polymer.dom(this).appendChild(this.layerTreeView_);
+ },
+
+ updateContents() {
+ const tile = this.objectSnapshot_;
+ const layerTreeHostImpl = tile.containingSnapshot;
+ if (!layerTreeHostImpl) return;
+
+ this.layerTreeView_.objectSnapshot = layerTreeHostImpl;
+ this.layerTreeView_.selection = new tr.ui.e.chrome.cc.TileSelection(tile);
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ TileSnapshotView,
+ {
+ typeName: 'cc::Tile',
+ showInTrackView: false
+ });
+
+ return {
+ TileSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html
new file mode 100644
index 00000000000..af6c79447bc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright 2018 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<dom-module id='tr-ui-e-chrome-codesearch'>
+ <template>
+ <style>
+ :host {
+ white-space: nowrap;
+ }
+ #codesearchLink {
+ font-size: x-small;
+ margin-left: 20px;
+ text-decoration: none;
+ }
+ </style>
+ <a id="codesearchLink" target=_blank on-click="onClick">&#x1F50D;</a>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome', function() {
+ Polymer({
+ is: 'tr-ui-e-chrome-codesearch',
+
+ set searchPhrase(phrase) {
+ const link = Polymer.dom(this.$.codesearchLink);
+ const codeSearchURL =
+ 'https://cs.chromium.org/search/?sq=package:chromium&type=cs&q=';
+ link.setAttribute('href', codeSearchURL + encodeURIComponent(phrase));
+ },
+
+ onClick(clickEvent) {
+ // Let the event trigger the default action of following the link. Stop
+ // the propagation of the event here, so that subsequent handlers do not
+ // intercept the clicks.
+ clickEvent.stopPropagation();
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html
new file mode 100644
index 00000000000..ec7991b6640
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/gpu/gpu_async_slice.html">
+<link rel="import" href="/tracing/extras/chrome/gpu/state.html">
+<link rel="import" href="/tracing/ui/extras/chrome/gpu/state_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png
new file mode 100644
index 00000000000..8ea9bc726bb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css
new file mode 100644
index 00000000000..7c2c34787dc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css
@@ -0,0 +1,15 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.tr-ui-e-chrome-gpu-state-snapshot-view {
+ background: url('./images/checkerboard.png');
+ display: flex;
+ overflow: auto;
+}
+
+.tr-ui-e-chrome-gpu-state-snapshot-view img {
+ display: block;
+ margin: 16px auto 16px auto;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html
new file mode 100644
index 00000000000..ba6c345be5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/extras/chrome/gpu/state_view.css">
+
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.chrome.gpu', function() {
+ /*
+ * Displays a GPU state snapshot in a human readable form.
+ * @constructor
+ */
+ const StateSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-chrome-gpu-state-snapshot-view',
+ tr.ui.analysis.ObjectSnapshotView);
+
+ StateSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add('tr-ui-e-chrome-gpu-state-snapshot-view');
+ this.screenshotImage_ = document.createElement('img');
+ Polymer.dom(this).appendChild(this.screenshotImage_);
+ },
+
+ updateContents() {
+ if (this.objectSnapshot_ && this.objectSnapshot_.screenshot) {
+ this.screenshotImage_.src = 'data:image/png;base64,' +
+ this.objectSnapshot_.screenshot;
+ }
+ }
+ };
+ tr.ui.analysis.ObjectSnapshotView.register(
+ StateSnapshotView,
+ {typeName: 'gpu::State'});
+
+ return {
+ StateSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html
new file mode 100644
index 00000000000..db80ef75afa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html
@@ -0,0 +1,229 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/chrome/layout_tree.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+
+<dom-module id='tr-ui-a-layout-tree-sub-view'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+ <div id="content"></div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.analysis', function() {
+ Polymer({
+ is: 'tr-ui-a-layout-tree-sub-view',
+ behaviors: ['tr-ui-a-sub-view'],
+
+ set selection(selection) {
+ this.currentSelection_ = selection;
+ this.updateContents_();
+ },
+
+ get selection() {
+ return this.currentSelection_;
+ },
+
+ updateContents_() {
+ this.set('$.content.textContent', '');
+ if (!this.currentSelection_) return;
+
+ const columns = [
+ {
+ title: 'Tag/Name',
+ value(layoutObject) {
+ return layoutObject.tag || ':' + layoutObject.name;
+ }
+ },
+
+ {
+ title: 'htmlId',
+ value(layoutObject) {
+ return layoutObject.htmlId || '';
+ }
+ },
+
+ {
+ title: 'classNames',
+ value(layoutObject) {
+ return layoutObject.classNames || '';
+ }
+ },
+
+ {
+ title: 'reasons',
+ value(layoutObject) {
+ return layoutObject.needsLayoutReasons.join(', ');
+ }
+ },
+
+ {
+ title: 'width',
+ value(layoutObject) {
+ return layoutObject.absoluteRect.width;
+ }
+ },
+
+ {
+ title: 'height',
+ value(layoutObject) {
+ return layoutObject.absoluteRect.height;
+ }
+ },
+
+ {
+ title: 'absX',
+ value(layoutObject) {
+ return layoutObject.absoluteRect.left;
+ }
+ },
+
+ {
+ title: 'absY',
+ value(layoutObject) {
+ return layoutObject.absoluteRect.top;
+ }
+ },
+
+ {
+ title: 'relX',
+ value(layoutObject) {
+ return layoutObject.relativeRect.left;
+ }
+ },
+
+ {
+ title: 'relY',
+ value(layoutObject) {
+ return layoutObject.relativeRect.top;
+ }
+ },
+
+ {
+ title: 'float',
+ value(layoutObject) {
+ return layoutObject.isFloat ? 'float' : '';
+ }
+ },
+
+ {
+ title: 'positioned',
+ value(layoutObject) {
+ return layoutObject.isPositioned ? 'positioned' : '';
+ }
+ },
+
+ {
+ title: 'relative',
+ value(layoutObject) {
+ return layoutObject.isRelativePositioned ? 'relative' : '';
+ }
+ },
+
+ {
+ title: 'sticky',
+ value(layoutObject) {
+ return layoutObject.isStickyPositioned ? 'sticky' : '';
+ }
+ },
+
+ {
+ title: 'anonymous',
+ value(layoutObject) {
+ return layoutObject.isAnonymous ? 'anonymous' : '';
+ }
+ },
+
+ {
+ title: 'row',
+ value(layoutObject) {
+ if (layoutObject.tableRow === undefined) {
+ return '';
+ }
+ return layoutObject.tableRow;
+ }
+ },
+
+ {
+ title: 'col',
+ value(layoutObject) {
+ if (layoutObject.tableCol === undefined) {
+ return '';
+ }
+ return layoutObject.tableCol;
+ }
+ },
+
+ {
+ title: 'rowSpan',
+ value(layoutObject) {
+ if (layoutObject.tableRowSpan === undefined) {
+ return '';
+ }
+ return layoutObject.tableRowSpan;
+ }
+ },
+
+ {
+ title: 'colSpan',
+ value(layoutObject) {
+ if (layoutObject.tableColSpan === undefined) {
+ return '';
+ }
+ return layoutObject.tableColSpan;
+ }
+ },
+
+ {
+ title: 'address',
+ value(layoutObject) {
+ return layoutObject.id.toString(16);
+ }
+ }
+ ];
+
+ const table = this.ownerDocument.createElement('tr-ui-b-table');
+ table.defaultExpansionStateCallback = function(
+ layoutObject, parentLayoutObject) {
+ return true;
+ };
+ table.subRowsPropertyName = 'childLayoutObjects';
+ table.tableColumns = columns;
+ table.tableRows = this.currentSelection_.map(function(snapshot) {
+ return snapshot.rootLayoutObject;
+ });
+ table.rebuild();
+ Polymer.dom(this.$.content).appendChild(table);
+ },
+ });
+
+ return {};
+});
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-layout-tree-sub-view',
+ tr.e.chrome.LayoutTreeSnapshot,
+ {
+ multi: false,
+ title: 'Layout Tree',
+ });
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-a-layout-tree-sub-view',
+ tr.e.chrome.LayoutTreeSnapshot,
+ {
+ multi: true,
+ title: 'Layout Trees',
+ });
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html
new file mode 100644
index 00000000000..cea42a2d78c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<!--
+The chrome config is heavily used:
+ - chrome://tracing,
+ - trace2html, which in turn implies
+ - adb_profile_chrome
+ - telemetry
+-->
+
+<!--
+TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the
+same way that all non-UI files depend on tracing/base/base.html. Enforce this
+dependency with a presubmit.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/extras/chrome_config.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html">
+<link rel="import" href="/tracing/ui/extras/chrome/codesearch.html">
+<link rel="import" href="/tracing/ui/extras/chrome/gpu/gpu.html">
+<link rel="import" href="/tracing/ui/extras/chrome/layout_tree_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/frame_data_side_panel.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/input_latency_side_panel.html">
+<link rel="import" href="/tracing/ui/extras/system_stats/system_stats.html">
+<link rel="import" href="/tracing/ui/extras/v8_config.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html
new file mode 100644
index 00000000000..cb3d41a88a0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<!--
+This class tries to (simply) copy the telemetry Results object, but outputs
+directly to an HTML table. It takes things that look like Telemetry values,
+and updates the table internally.
+-->
+<dom-module id='tr-ui-e-deep-reports-html-results'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ font-size: 12px;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-deep-reports-html-results',
+
+ created() {
+ this.hasColumnNamed_ = {};
+ this.pageToRowMap_ = new WeakMap();
+ },
+
+ ready() {
+ const table = this.$.table;
+ table.tableColumns = [
+ {
+ title: 'Label',
+ value(row) { return row.label; },
+ width: '350px'
+ }
+ ];
+ this.clear();
+ },
+
+ clear() {
+ this.$.table.tableRows = [];
+ },
+
+ addColumnIfNeeded_(columnName) {
+ if (this.hasColumnNamed_[columnName]) return;
+
+ this.hasColumnNamed_[columnName] = true;
+
+ const column = {
+ title: columnName,
+ value(row) {
+ if (row[columnName] === undefined) return '';
+ return row[columnName];
+ }
+ };
+
+ const columns = this.$.table.tableColumns;
+ columns.push(column);
+
+ // Update widths.
+ let colWidthPercentage;
+ if (columns.length === 1) {
+ colWidthPercentage = '100%';
+ } else {
+ colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%';
+ }
+
+ for (let i = 1; i < columns.length; i++) {
+ columns[i].width = colWidthPercentage;
+ }
+
+ this.$.table.tableColumns = columns;
+ },
+
+ getRowForPage_(page) {
+ if (!this.pageToRowMap_.has(page)) {
+ const i = page.url.lastIndexOf('/');
+ const baseName = page.url.substring(i + 1);
+
+ const link = document.createElement('a');
+ link.href = 'trace_viewer.html#' + page.url;
+ Polymer.dom(link).textContent = baseName;
+
+ const row = {
+ label: link,
+ value: '',
+ subRows: [],
+ isExpanded: true
+ };
+ this.$.table.tableRows.push(row);
+ this.pageToRowMap_.set(page, row);
+
+ // Kick table rebuild.
+ this.$.table.tableRows = this.$.table.tableRows;
+ }
+ return this.pageToRowMap_.get(page);
+ },
+
+ addValue(value) {
+ /* Value is expected to be a scalar telemetry-style Value. */
+ if (value.type !== 'scalar') {
+ throw new Error('wat');
+ }
+
+ this.addColumnIfNeeded_(value.name);
+ const rowForPage = this.getRowForPage_(value.page);
+ rowForPage[value.name] = value.value;
+
+ // Kick table rebuild.
+ this.$.table.tableRows = this.$.table.tableRows;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html
new file mode 100644
index 00000000000..9cf2d7f6c79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/ui/extras/deep_reports/scalar_value.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.deep_reports', function() {
+ /**
+ * Runs deep reports on the provided files, and pushes telemetry-style
+ * values to the results object.
+ */
+ function main(results, filesInDir) {
+ let lastP = new Promise(function(resolve) { resolve(); });
+
+ filesInDir.forEach(function(filename) {
+ // TODO(nduca): Make this like telemetry page.
+ const page = {
+ url: filename
+ };
+ lastP = lastP.then(function() {
+ return loadModelFromFileAsync(filename);
+ });
+ lastP = lastP.then(function(model) {
+ processModel(results, page, model);
+ });
+ });
+ return lastP;
+ }
+
+ function loadModelFromFileAsync(filename) {
+ return tr.b.getAsync(filename).then(function(trace) {
+ const io = new tr.ImportOptions();
+ io.shiftWorldToZero = true;
+ io.pruneEmptyContainers = false;
+
+ const m = new tr.Model();
+ try {
+ m.importTraces([trace], io);
+ } catch (e) {
+ throw new Error('While loading ' + filename + ' got: ' + e.toString());
+ }
+ return m;
+ });
+ }
+
+ function processModel(results, page, model) {
+ results.addValue(
+ new tr.ui.e.deep_reports.ScalarValue(
+ page, 'numRailIRs', 'ms', model.userModel.expectations.length));
+ }
+
+ return {
+ main
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html
new file mode 100644
index 00000000000..cb550c35b4b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.deep_reports', function() {
+ function ScalarValue(page, name, units, value,
+ opt_important, opt_description) {
+ this.type = 'scalar';
+ this.page = page;
+ this.name = name;
+ this.units = units;
+ this.value = value;
+ this.important = opt_important !== undefined ? opt_important : false;
+ this.description = opt_description || '';
+ }
+ ScalarValue.fromDict = function(page, dict) {
+ if (dict.type !== 'scalar') {
+ throw new Error('wat');
+ }
+ const v = new ScalarValue(page, dict.name, dict.units, dict.value);
+ v.important = dict.important;
+ v.description = dict.description;
+ v.value = dict.value;
+ return v;
+ };
+
+ ScalarValue.prototype = {
+
+ };
+
+ return {
+ ScalarValue,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html
new file mode 100644
index 00000000000..1748dd12b88
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<dom-module id='tr-ui-e-drive-comment-element'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ #comment-area {
+ display: flex;
+ flex-direction: column;
+ border-top: 1px solid #e8e8e8;
+ background-color: white;
+ padding: 6px;
+ margin-bottom: 4px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
+ border-radius: 2px;
+ font-size: small;
+ }
+ #comment-header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-bottom: 8px;
+ }
+ #comment-header-text {
+ display: flex;
+ flex-direction: column;
+ padding-left: 10px;
+ }
+ #comment-img {
+ width: 32px;
+ height: 32px;
+ }
+ #comment-text-author {
+ padding-bottom: 2px;
+ }
+ #comment-date {
+ color: #777;
+ font-size: 11px;
+ }
+ #comment-content {
+ word-wrap: break-word;
+ }
+ </style>
+ <div id="comment-area">
+ <div id="comment-header">
+ <img id="comment-img" src="{{ comment.author.picture.url }}" />
+ <div id="comment-header-text">
+ <div id="comment-text-author">{{ comment.author.displayName }}</div>
+ <div id="comment-date">{{ createdDate }}</div>
+ </div>
+ </div>
+ <div id="comment-content">{{_computeCommentContentPrefix( comment)}}
+ {{ comment.content }}</div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-drive-comment-element',
+
+ properties: {
+ comment: {
+ type: String,
+ observer: '_commentChanged'
+ }
+ },
+
+ _commentChanged() {
+ this.createdDate = new Date(this.comment.createdDate).toLocaleString();
+ },
+
+ _computeCommentContentPrefix(comment) {
+ return comment.anchor ? '&#9875;&nbsp;' : '';
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html
new file mode 100644
index 00000000000..8f52839029d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/extras/drive/comment_element.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<dom-module id='tr-ui-e-drive-comments-side-panel'>
+ <template>
+ <style>
+ :host {
+ flex-direction: column;
+ display: flex;
+ width: 290px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ background-color: #eee;
+ }
+ toolbar {
+ flex: 0 0 auto;
+ border-bottom: 1px solid black;
+ display: flex;
+ }
+ result-area {
+ flex: 1 1 auto;
+ display: block;
+ min-height: 0;
+ padding: 4px;
+ }
+ #comments-textarea-container {
+ display: flex;
+ }
+ #commentinput {
+ width: 100%;
+ }
+ </style>
+
+ <toolbar id='toolbar'></toolbar>
+ <result-area id='result_area'>
+ <template is="dom-repeat" items="{{comments_}}" repeat="{{ comment in comments_ }}">
+ <tr-ui-e-drive-comment-element comment="{{comment}}"
+ on-click="commentClick">
+ </tr-ui-e-drive-comment-element>
+ </template>
+ <div id="comments-textarea-container">
+ <textarea id="commentinput" on-focus='textAreaFocus'
+ on-blur='textAreaBlur'
+ on-keypress="textareaKeypress"></textarea>
+ </div>
+ </result-area>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-drive-comments-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+ ready() {
+ this.rangeOfInterest_ = new tr.b.math.Range();
+ this.selection_ = undefined;
+ this.comments_ = [];
+ this.annotationFromComment_ = undefined;
+ this.textAreaFocused = false;
+ },
+
+ setCommentProvider(commentProvider) {
+ this.commentProvider_ = commentProvider;
+ },
+
+ attached() {
+ if (this.commentProvider_ === undefined) {
+ this.commentProvider_ =
+ new tr.ui.e.drive.analysis.DefaultCommentProvider();
+ }
+ this.commentProvider_.attachToElement(this);
+ },
+
+ detached() {
+ this.commentProvider_.detachFromElement();
+ },
+
+ commentClick(event) {
+ const anchor = event.currentTarget.comment.anchor;
+ if (!anchor) return;
+
+ const uiState =
+ JSON.parse(anchor).a[0][tr.ui.e.drive.constants.ANCHOR_NAME];
+
+ const myEvent = new CustomEvent('navigateToUIState', { detail:
+ new tr.ui.b.UIState(new tr.model.Location(uiState.location.xWorld,
+ uiState.location.yComponents),
+ uiState.scaleX)
+ });
+ document.dispatchEvent(myEvent);
+
+ if (this.annotationFromComment_) {
+ this.model.removeAnnotation(this.annotationFromComment_);
+ }
+ const loc = new tr.model.Location(uiState.location.xWorld,
+ uiState.location.yComponents);
+
+ const text = sender.comment.author.displayName + ': ' +
+ sender.comment.content;
+ this.annotationFromComment_ =
+ new tr.model.CommentBoxAnnotation(loc, text);
+ this.model.addAnnotation(this.annotationFromComment_);
+ },
+
+ textareaKeypress(event) {
+ // Check for return key.
+ if (event.keyCode === 13 && !event.ctrlKey) {
+ this.commentProvider_.addComment(this.$.commentinput.value);
+ this.$.commentinput.value = '';
+ }
+ event.stopPropagation();
+ return true;
+ },
+
+ textAreaFocus(event) {
+ this.textAreaFocused = true;
+ },
+
+ textAreaBlur(event) {
+ this.textAreaFocused = false;
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ this.rangeOfInterest_ = rangeOfInterest;
+ this.updateContents_();
+ },
+
+ get currentRangeOfInterest() {
+ if (this.rangeOfInterest_.isEmpty) {
+ return this.model_.bounds;
+ }
+ return this.rangeOfInterest_;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+ },
+
+ updateContents_() {
+ this.commentProvider_.updateComments();
+ },
+
+ supportsModel(m) {
+ if (m === undefined) {
+ return {
+ supported: false,
+ reason: 'Unknown tracing model'
+ };
+ }
+ return {
+ supported: true
+ };
+ },
+
+ get textLabel() {
+ return 'Comments';
+ }
+});
+
+tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-e-drive-comments-side-panel');
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html
new file mode 100644
index 00000000000..639c1b9b597
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/extras/drive/comments_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function StubCommentProvider() {
+ this.addDummyComment('Lorem ipsum dolor sit amet');
+ this.addDummyComment('consectetur adipiscing elit');
+ this.addDummyComment('sed do eiusmod tempor incididunt ut labore et ' +
+ 'dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' +
+ 'exercitation ullamco laboris nisi ut aliquip ex ea commodo ' +
+ 'consequat. Duis aute irure dolor in reprehenderit in voluptate ' +
+ 'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint ' +
+ 'occaecat cupidatat non proident, sunt in culpa qui officia deserunt ' +
+ 'mollit anim id est laborum.');
+ }
+
+ StubCommentProvider.prototype = {
+ comments_: [],
+
+ attachToElement(attachedElement) {
+ this.attachedElement_ = attachedElement;
+ this.updateComments();
+ },
+
+ detachFromElement() {
+ },
+
+ updateComments() {
+ this.attachedElement_.comments_ = this.comments_;
+ },
+
+ addDummyComment(content) {
+ const newComment = {
+ author: {
+ displayName: 'Casper the Friendly Ghost',
+ picture: {
+ url: 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/' +
+ 'AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/s128/photo.jpg'
+ }
+ },
+ createdDate: Date.now(),
+ anchor: (this.comments_.length) % 2 ? 1 : 0,
+ content
+ };
+
+ this.comments_.push(newComment);
+ },
+
+ addComment(body) {
+ this.addDummyComment(body);
+ this.updateComments();
+ }
+ };
+
+ test('instantiate', function() {
+ const panel = document.createElement('tr-ui-e-drive-comments-side-panel');
+ panel.setCommentProvider(new StubCommentProvider);
+ this.addHTMLOutput(panel);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html
new file mode 100644
index 00000000000..42d3706f854
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/comment_box_annotation.html">
+
+<link rel="import" href="/tracing/ui/extras/drive/comments_side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+
+<script>
+'use strict';
+
+(function() {
+ function addDriveCommentWithUIState_(text, uiState) {
+ gapi.client.load('drive', 'v2', function() {
+ const request = gapi.client.drive.revisions.get({
+ 'fileId': tr.ui.e.drive.getDriveFileId(),
+ 'revisionId': 'head'
+ });
+ request.execute(function(resp) {
+ const anchorObject = {};
+ anchorObject[tr.ui.e.drive.constants.ANCHOR_NAME] = uiState;
+ let anchor = {
+ 'r': resp.id,
+ 'a': [anchorObject]
+ };
+ anchor = JSON.stringify(anchor);
+ gapi.client.load('drive', 'v2', function() {
+ const request = gapi.client.drive.comments.insert({
+ 'fileId': tr.ui.e.drive.getDriveFileId(),
+ 'resource': {'content': text, anchor}
+ });
+ request.execute();
+ });
+ });
+ });
+ }
+
+ function onCommentWithUIState(e) {
+ addDriveCommentWithUIState_(e.detail.name, e.detail.location);
+ }
+
+ document.addEventListener('commentWithUIState',
+ onCommentWithUIState.bind(this));
+}());
+
+tr.exportTo('tr.ui.e.drive.analysis', function() {
+ function DefaultCommentProvider() { }
+
+ DefaultCommentProvider.prototype = {
+ attachToElement(attachedElement) {
+ this.attachedElement_ = attachedElement;
+ this.commentsCheckTimer_ = setTimeout(this.checkForComments_.bind(this),
+ 5000);
+ },
+
+ detachFromElement() {
+ clearTimeout(this.commentsCheckTimer_);
+ },
+
+ checkForComments_() {
+ this.updateComments();
+ this.commentsCheckTimer_ = setTimeout(this.checkForComments_.bind(this),
+ 5000);
+ },
+
+ updateComments() {
+ gapi.client.load('drive', 'v2', () => {
+ const request = gapi.client.drive.comments.list({
+ 'fileId': tr.ui.e.drive.getDriveFileId()
+ });
+ request.execute(results => {
+ this.attachedElement_.comments_ = results.items;
+ });
+ });
+ },
+
+ addComment(body) {
+ gapi.client.load('drive', 'v2', () => {
+ const request = gapi.client.drive.comments.insert({
+ 'fileId': tr.ui.e.drive.getDriveFileId(),
+ 'resource': {'content': body}
+ });
+ request.execute(resp => {
+ this.updateComments();
+ });
+ });
+ }
+ };
+
+ return {
+ DefaultCommentProvider,
+ };
+});
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html
new file mode 100644
index 00000000000..270dcccf2fc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html
@@ -0,0 +1,463 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+ <script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
+
+ <link rel="import" href="/components/polymer/polymer.html">
+ <link rel="import" href="/tracing/ui/extras/drive/drive_comment_provider.html">
+ <link rel="import" href="/tracing/ui/extras/full_config.html">
+ <link rel="import" href="/tracing/ui/timeline_view.html">
+
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+ body > x-timeline-view {
+ flex: 1 1 auto;
+ overflow: hidden;
+ position: absolute;
+ top: 0px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+ body > x-timeline-view:focus {
+ outline: none;
+ }
+ nav {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ }
+ #navbar button {
+ height: 24px;
+ padding-bottom: 3px;
+ vertical-align: middle;
+ box-shadow: none;
+ background-color: #4d90fe;
+ background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed);
+ border: 1px solid #3079ed;
+ color: #fff;
+ border-radius: 2px;
+ cursor: default;
+ font-size: 11px;
+ font-weight: bold;
+ text-align: center;
+ white-space: nowrap;
+ line-height: 27px;
+ min-width: 54px;
+ outline: 0px;
+ padding: 0 8px;
+ font: normal 13px arial,sans-serif;
+ margin: 5px;
+ }
+ #collabs {
+ display: flex;
+ flex-direction: row;
+ }
+ .collaborator-div {
+ display: inline-block;
+ vertical-align: middle;
+ min-height: 0;
+ width: 100px;
+ font-size: 11px;
+ font-weight: bold;
+ font: normal 13px arial,sans-serif;
+ margin: 10px;
+ }
+ .collaborator-img {
+ margin: 2px;
+ }
+ .collaborator-tooltip {
+ z-index: 10000;
+ transition: visibility 0,opacity .13s ease-in;
+ background-color: #2a2a2a;
+ border: 1px solid #fff;
+ color: #fff;
+ cursor: default;
+ display: block;
+ font-family: arial, sans-serif;
+ font-size: 11px;
+ font-weight: bold;
+ margin-left: -1px;
+ opacity: 1;
+ padding: 7px 9px;
+ word-break: break-word;
+ position: absolute;
+ }
+ .collaborator-tooltip-content {
+ color: #fff;
+ }
+ .collaborator-tooltip-arrow {
+ position: absolute;
+ top: -6px;
+ }
+ .collaborator-tooltip-arrow-before {
+ border-color: #fff transparent !important;
+ left: -6px;
+ border: 6px solid;
+ border-top-width: 0;
+ content: '';
+ display: block;
+ height: 0;
+ position: absolute;
+ width: 0;
+ }
+ .collaborator-tooltip-arrow-after {
+ top: 1px;
+ border-color: #2a2a2a transparent !important;
+ left: -5px;
+ border: 5px solid;
+ border-top-width: 0;
+ content: '';
+ display: block;
+ height: 0;
+ position: absolute;
+ width: 0;
+ }
+
+ </style>
+ <title>Trace Viewer</title>
+</head>
+<body>
+ <nav id="navbar">
+ <div id="collabs"></div>
+ <button id="x-drive-save-to-disk">Save to disk</button>
+ <button id="x-drive-save-to-drive">Save to Drive</button>
+ <button id="x-drive-load-from-drive">Load from Drive</button>
+ <button id="x-drive-share">Share</button>
+ </nav>
+ <x-timeline-view>
+ </x-timeline-view>
+
+ <script>
+ 'use strict';
+
+ // Needs to be global as it's passed through the Google API as a
+ // GET parameter.
+ let onAPIClientLoaded_ = null;
+
+ (function() {
+ tr.exportTo('tr.ui.e.drive', function() {
+ const appId = '239864068844';
+ const constants = {
+ APP_ID: appId,
+ ANCHOR_NAME: appId + '.trace_viewer',
+ DEVELOPER_KEY: 'AIzaSyDR-6_wL9vHg1_oz4JHk8IQAkv2_Y0Y8-M',
+ CLIENT_ID: '239864068844-c7gefbfdcp0j6grltulh2r88tsvl18c1.apps.' +
+ 'googleusercontent.com',
+ SCOPE: [
+ 'https://www.googleapis.com/auth/drive',
+ 'https://www.googleapis.com/auth/drive.install',
+ 'https://www.googleapis.com/auth/drive.file',
+ 'profile'
+ ]
+ };
+
+ return {
+ getDriveFileId() { return driveFileId_; },
+ constants
+ };
+ });
+
+
+ let pickerApiLoaded_ = false;
+ let oauthToken_ = null;
+
+ let timelineViewEl_ = null;
+ let driveDocument_ = null;
+ let shareClient_ = null;
+ let fileIdToLoad_ = null;
+ let driveFileId_ = null;
+
+ function parseGETParameter(val) {
+ let result = null;
+ let tmp = [];
+ location.search.substr(1).split('&').forEach(function(item) {
+ tmp = item.split('=');
+ if (tmp[0] === val) {
+ result = decodeURIComponent(tmp[1]);
+ }
+ });
+ return result;
+ }
+
+ // Use the Google API Loader script to load the google.picker script.
+ onAPIClientLoaded_ = function() {
+ const driveState = parseGETParameter('state');
+ if (driveState !== null) {
+ const driveStateJson = JSON.parse(driveState);
+ fileIdToLoad_ = String(driveStateJson.ids);
+ }
+
+ gapi.load('picker', {'callback': onPickerApiLoad});
+ gapi.load('auth', {'callback'() {
+ onAuthApiLoad(true, onAuthResultSuccess);
+ return tr.b.timeout(30e3)
+ .then(() => onAuthApiLoad(true, function() {}))
+ .then(() => tr.b.timeout(30e3))
+ .then(() => onRepeatAuthApiLoad);
+ }});
+ };
+
+ function onAuthApiLoad(tryImmediate, resultCallback) {
+ window.gapi.auth.authorize(
+ {'client_id': tr.ui.e.drive.constants.CLIENT_ID,
+ 'scope': tr.ui.e.drive.constants.SCOPE, 'immediate': tryImmediate},
+ function(authResult) {
+ handleAuthResult(authResult, tryImmediate, resultCallback);
+ });
+ }
+
+ function onPickerApiLoad() {
+ pickerApiLoaded_ = true;
+ if (fileIdToLoad_ === null) {
+ createPicker();
+ }
+ }
+
+ function onAuthResultSuccess() {
+ if (fileIdToLoad_ === null) {
+ createPicker();
+ } else {
+ loadFileFromDrive(fileIdToLoad_);
+ }
+ }
+
+ function handleAuthResult(authResult, wasImmediate, resultCallback) {
+ if (authResult && !authResult.error) {
+ oauthToken_ = authResult.access_token;
+ resultCallback();
+ } else if (wasImmediate) {
+ onAuthApiLoad(false);
+ }
+ }
+
+ function createPicker() {
+ if (pickerApiLoaded_ && oauthToken_) {
+ const view = new google.picker.View(google.picker.ViewId.DOCS);
+ view.setMimeTypes('application/json,application/octet-stream');
+ const picker = new google.picker.PickerBuilder()
+ .enableFeature(google.picker.Feature.NAV_HIDDEN)
+ .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
+ .setAppId(tr.ui.e.drive.constants.APP_ID)
+ .setOAuthToken(oauthToken_)
+ .addView(view)
+ .addView(new google.picker.DocsUploadView())
+ .setDeveloperKey(tr.ui.e.drive.constants.DEVELOPER_KEY)
+ .setCallback(pickerCallback)
+ .build();
+ picker.setVisible(true);
+ }
+ }
+
+ function pickerCallback(data) {
+ if (data.action === google.picker.Action.PICKED) {
+ loadFileFromDrive(data.docs[0].id);
+ }
+ }
+
+ function initShareButton() {
+ shareClient_ = new gapi.drive.share.ShareClient(
+ tr.ui.e.drive.constants.APP_ID);
+ shareClient_.setItemIds([driveFileId_]);
+ }
+
+ function loadFileFromDrive(fileId) {
+ gapi.client.load('drive', 'v2', function() {
+ const request = gapi.client.drive.files.get({fileId});
+ request.execute(function(resp) { downloadFile(resp); });
+ driveFileId_ = fileId;
+ gapi.load('drive-share', initShareButton);
+ });
+ }
+
+ function downloadFile(file) {
+ if (file.downloadUrl) {
+ const downloadingOverlay = tr.ui.b.Overlay();
+ downloadingOverlay.title = 'Downloading...';
+ downloadingOverlay.userCanClose = false;
+ downloadingOverlay.msgEl = document.createElement('div');
+ Polymer.dom(downloadingOverlay).appendChild(downloadingOverlay.msgEl);
+ downloadingOverlay.msgEl.style.margin = '20px';
+ downloadingOverlay.update = function(msg) {
+ Polymer.dom(this.msgEl).textContent = msg;
+ };
+ downloadingOverlay.visible = true;
+
+ const accessToken = gapi.auth.getToken().access_token;
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', file.downloadUrl);
+ xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
+ xhr.onload = function() {
+ downloadingOverlay.visible = false;
+ onDownloaded(file.title, xhr.responseText);
+ };
+ xhr.onprogress = function(evt) {
+ downloadingOverlay.update(
+ Math.floor(evt.position * 100 / file.fileSize) + '% complete');
+ };
+ xhr.onerror = function() { alert('Failed downloading!'); };
+ xhr.send();
+ } else {
+ alert('No URL!');
+ }
+ }
+
+ function displayAllCollaborators() {
+ const allCollaborators = driveDocument_.getCollaborators();
+ const collaboratorCount = allCollaborators.length;
+ const collabspan = document.getElementById('collabs');
+ Polymer.dom(collabspan).innerHTML = '';
+ const imageList = [];
+ for (let i = 0; i < collaboratorCount; i++) {
+ const user = allCollaborators[i];
+
+ const img = document.createElement('img');
+ img.src = user.photoUrl;
+ img.alt = user.displayName;
+ img.height = 30;
+ img.width = 30;
+ img.className = 'collaborator-img';
+ Polymer.dom(collabspan).appendChild(img);
+ imageList.push({'image': img, 'name': user.displayName});
+ }
+ for (i = 0; i < imageList.length; i++) {
+ const collabTooltip = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip'
+ });
+ const collabTooltipContent = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip-content'
+ });
+ Polymer.dom(collabTooltipContent).textContent = imageList[i].name;
+ Polymer.dom(collabTooltip).appendChild(collabTooltipContent);
+ Polymer.dom(collabspan).appendChild(collabTooltip);
+ const collabTooltipArrow = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip-arrow'});
+ Polymer.dom(collabTooltip).appendChild(collabTooltipArrow);
+ const collabTooltipArrowBefore = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip-arrow-before'});
+ Polymer.dom(collabTooltipArrow).appendChild(collabTooltipArrowBefore);
+ const collabTooltipArrowAfter = tr.ui.b.createDiv({
+ className: 'collaborator-tooltip-arrow-after'});
+ Polymer.dom(collabTooltipArrow).appendChild(collabTooltipArrowAfter);
+
+ const rect = imageList[i].image.getBoundingClientRect();
+ collabTooltip.style.top = (rect.bottom - 6) + 'px';
+ collabTooltip.style.left =
+ (rect.left + 16 - (collabTooltip.offsetWidth / 2)) + 'px';
+ collabTooltipArrow.style.left = (collabTooltip.offsetWidth / 2) + 'px';
+ collabTooltip.style.visibility = 'hidden';
+ function visibilityDelegate(element, visibility) {
+ return function() {
+ element.style.visibility = visibility;
+ };
+ }
+ imageList[i].image.addEventListener(
+ 'mouseover', visibilityDelegate(collabTooltip, 'visible'));
+ imageList[i].image.addEventListener(
+ 'mouseout', visibilityDelegate(collabTooltip, 'hidden'));
+ }
+ }
+
+ function onRealtimeFileLoaded(doc) {
+ if (driveDocument_) {
+ driveDocument_.close();
+ }
+ driveDocument_ = doc;
+ doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_JOINED,
+ displayAllCollaborators);
+ doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_LEFT,
+ displayAllCollaborators);
+
+ displayAllCollaborators(doc);
+ }
+
+ function onRealtimeError(e) {
+ alert('Error loading realtime: ' + e);
+ }
+
+ function onDownloaded(filename, content) {
+ gapi.load('auth:client,drive-realtime,drive-share', function() {
+ gapi.drive.realtime.load(driveFileId_,
+ onRealtimeFileLoaded,
+ null,
+ onRealtimeError);
+ });
+
+ const traces = [];
+ const filenames = [];
+ filenames.push(filename);
+ traces.push(content);
+ createViewFromTraces(filenames, traces);
+ }
+
+ function createViewFromTraces(filenames, traces) {
+ const m = new tr.Model();
+ const i = new tr.importer.Import(m);
+ const p = i.importTracesWithProgressDialog(traces);
+ p.then(
+ function() {
+ timelineViewEl_.model = m;
+ timelineViewEl_.updateDocumentFavicon();
+ timelineViewEl_.globalMode = true;
+ timelineViewEl_.viewTitle = '';
+ },
+ function(err) {
+ const downloadingOverlay = new tr.ui.b.Overlay();
+ Polymer.dom(downloadingOverlay).textContent =
+ tr.b.normalizeException(err).message;
+ downloadingOverlay.title = 'Import error';
+ downloadingOverlay.visible = true;
+ });
+ }
+
+ function onSaveToDiskClicked() {
+ throw new Error('Not implemented');
+ }
+
+ function onSaveToDriveClicked() {
+ throw new Error('Not implemented');
+ }
+
+ function onLoadFromDriveClicked() {
+ createPicker();
+ }
+
+ function onLoad() {
+ timelineViewEl_ = Polymer.dom(document).querySelector('x-timeline-view');
+ timelineViewEl_.globalMode = true;
+ const navbar = document.getElementById('navbar');
+ timelineViewEl_.style.top = navbar.offsetHeight + 'px';
+ tr.ui.b.decorate(timelineViewEl_, tr.ui.TimelineView);
+ }
+
+ window.addEventListener('load', onLoad);
+
+ document.getElementById('x-drive-save-to-disk').onclick =
+ onSaveToDiskClicked;
+ document.getElementById('x-drive-save-to-drive').onclick =
+ onSaveToDriveClicked;
+ document.getElementById('x-drive-load-from-drive').onclick =
+ onLoadFromDriveClicked;
+ document.getElementById('x-drive-share').onclick = function() {
+ shareClient_.showSettingsDialog();
+ };
+ }());
+
+ </script>
+ <script type="text/javascript"
+ src="https://apis.google.com/js/client.js?onload=onAPIClientLoaded_">
+ </script>
+</body>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html
new file mode 100644
index 00000000000..6d1e29d4e20
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<!--
+TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the
+same way that all non-UI files depend on tracing/base/base.html. Enforce this
+dependency with a presubmit.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<!-- The full config is all the configs slammed together. -->
+<link rel="import" href="/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html">
+<link rel="import" href="/tracing/ui/extras/chrome_config.html">
+<link rel="import" href="/tracing/ui/extras/lean_config.html">
+<link rel="import" href="/tracing/ui/extras/systrace_config.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html
new file mode 100644
index 00000000000..8d66352f140
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<!--
+TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the same way that
+all non-UI files depend on tracing/base/base.html. Enforce this dependency with a presubmit.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/extras/lean_config.html" data-suppress-import-order>
+
+<!--
+The lean config is just enough to import uncompressed, trace-event-formatted
+json blobs.
+-->
+<link rel="import" href="/tracing/ui/side_panel/file_size_stats_side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/metrics_side_panel.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html
new file mode 100644
index 00000000000..0971d7f5409
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html
@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<dom-module id='tr-ui-e-s-alerts-side-panel'>
+ <template>
+ <style>
+ :host {
+ display: block;
+ width: 250px;
+ }
+ #content {
+ flex-direction: column;
+ display: flex;
+ }
+ tr-ui-b-table {
+ font-size: 12px;
+ }
+ </style>
+
+ <div id='content'>
+ <toolbar id='toolbar'></toolbar>
+ <result-area id='result_area'></result-area>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-s-alerts-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+
+ ready() {
+ this.rangeOfInterest_ = new tr.b.math.Range();
+ this.selection_ = undefined;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+ },
+
+ set selection(selection) {
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ },
+
+ /**
+ * Fires a selection event selecting all alerts of the specified
+ * type.
+ */
+ selectAlertsOfType(alertTypeString) {
+ const alertsOfType = this.model_.alerts.filter(function(alert) {
+ return alert.title === alertTypeString;
+ });
+
+ const event = new tr.model.RequestSelectionChangeEvent();
+ event.selection = new tr.model.EventSet(alertsOfType);
+ this.dispatchEvent(event);
+ },
+
+ /**
+ * Returns a map for the specified alerts where each key is the
+ * alert type string and each value is a list of alerts with that
+ * type.
+ */
+ alertsByType_(alerts) {
+ const alertsByType = {};
+ alerts.forEach(function(alert) {
+ if (!alertsByType[alert.title]) {
+ alertsByType[alert.title] = [];
+ }
+
+ alertsByType[alert.title].push(alert);
+ });
+ return alertsByType;
+ },
+
+ alertsTableRows_(alertsByType) {
+ return Object.keys(alertsByType).map(function(key) {
+ return {
+ alertType: key,
+ count: alertsByType[key].length
+ };
+ });
+ },
+
+ alertsTableColumns_() {
+ return [
+ {
+ title: 'Alert type',
+ value(row) { return row.alertType; },
+ width: '180px'
+ },
+ {
+ title: 'Count',
+ width: '100%',
+ value(row) { return row.count; }
+ }
+ ];
+ },
+
+ createAlertsTable_(alerts) {
+ const alertsByType = this.alertsByType_(alerts);
+
+ const table = document.createElement('tr-ui-b-table');
+ table.tableColumns = this.alertsTableColumns_();
+ table.tableRows = this.alertsTableRows_(alertsByType);
+ table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ table.addEventListener('selection-changed', function(e) {
+ const row = table.selectedTableRow;
+ if (row) {
+ this.selectAlertsOfType(row.alertType);
+ }
+ }.bind(this));
+
+ return table;
+ },
+
+ updateContents_() {
+ Polymer.dom(this.$.result_area).textContent = '';
+ if (this.model_ === undefined) return;
+
+ const panel = this.createAlertsTable_(this.model_.alerts);
+ Polymer.dom(this.$.result_area).appendChild(panel);
+ },
+
+ supportsModel(m) {
+ if (m === undefined) {
+ return {
+ supported: false,
+ reason: 'Unknown tracing model'
+ };
+ } else if (m.alerts.length === 0) {
+ return {
+ supported: false,
+ reason: 'No alerts in tracing model'
+ };
+ }
+
+ return {
+ supported: true
+ };
+ },
+
+ get textLabel() {
+ return 'Alerts';
+ }
+});
+
+tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-e-s-alerts-side-panel');
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html
new file mode 100644
index 00000000000..c4cb9825d1e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/alerts_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'Critical alert');
+ const ALERT_INFO_2 = new tr.model.EventInfo(
+ 'Alert 2', 'Warning alert');
+
+ test('instantiate', function() {
+ const panel = document.createElement('tr-ui-e-s-alerts-side-panel');
+ panel.model = createModelWithAlerts([
+ new tr.model.Alert(ALERT_INFO_1, 5),
+ new tr.model.Alert(ALERT_INFO_2, 35)
+ ]);
+ panel.style.height = '100px';
+
+ this.addHTMLOutput(panel);
+ });
+
+ test('selectAlertsOfType', function() {
+ const panel = document.createElement('tr-ui-e-s-alerts-side-panel');
+ const alerts = [
+ new tr.model.Alert(ALERT_INFO_1, 1),
+ new tr.model.Alert(ALERT_INFO_1, 2),
+ new tr.model.Alert(ALERT_INFO_2, 3)
+ ];
+
+ const predictedAlerts = new tr.model.EventSet([alerts[0], alerts[1]]);
+ panel.model = createModelWithAlerts(alerts);
+ panel.style.height = '100px';
+ this.addHTMLOutput(panel);
+
+ let selectionChanged = false;
+ panel.addEventListener('requestSelectionChange', function(e) {
+ selectionChanged = true;
+ assert.isTrue(e.selection.equals(predictedAlerts));
+ });
+ panel.selectAlertsOfType(ALERT_INFO_1.title);
+
+ assert.isTrue(selectionChanged);
+ });
+
+ function createModelWithAlerts(alerts) {
+ const m = new tr.Model();
+ m.alerts = alerts;
+ return m;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html
new file mode 100644
index 00000000000..e5fd7689479
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html
@@ -0,0 +1,347 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-e-s-frame-data-side-panel'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ width: 600px;
+ flex-direction: column;
+ }
+ table-container {
+ display: flex;
+ overflow: auto;
+ font-size: 12px;
+ }
+ </style>
+ <div>
+ Organize by:
+ <select id="select">
+ <option value="none">None</option>
+ <option value="tree">Frame Tree</option>
+ </select>
+ </div>
+ <table-container>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </table-container>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.ui.e.s', function() {
+ const BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot;
+ const FrameTreeNodeSnapshot = tr.e.chrome.FrameTreeNodeSnapshot;
+ const RenderFrameSnapshot = tr.e.chrome.RenderFrameSnapshot;
+ const TopLevelSnapshot = tr.e.chrome.TopLevelSnapshot;
+
+ const BlameContextInstance = tr.e.chrome.BlameContextInstance;
+ const FrameTreeNodeInstance = tr.e.chrome.FrameTreeNodeInstance;
+ const RenderFrameInstance = tr.e.chrome.RenderFrameInstance;
+ const TopLevelInstance = tr.e.chrome.TopLevelInstance;
+
+ /**
+ * @constructor
+ * If |context| is provided, creates a row for the given context.
+ * Otherwise, creates an empty Row template which can be used for aggregating
+ * data from a group of subrows.
+ */
+ function Row(context) {
+ this.subRows = undefined;
+ this.contexts = [];
+ this.type = undefined;
+ this.renderer = 'N/A';
+ this.url = undefined;
+ this.time = 0;
+ this.eventsOfInterest = new tr.model.EventSet();
+
+ if (context === undefined) return;
+
+ this.type = context.objectInstance.blameContextType;
+ this.contexts.push(context);
+ if (context instanceof FrameTreeNodeSnapshot) {
+ if (context.renderFrame) {
+ this.contexts.push(context.renderFrame);
+ this.renderer = context.renderFrame.objectInstance.parent.pid;
+ }
+ } else if (context instanceof RenderFrameSnapshot) {
+ if (context.frameTreeNode) {
+ this.contexts.push(context.frameTreeNode);
+ }
+ this.renderer = context.objectInstance.parent.pid;
+ } else if (context instanceof TopLevelSnapshot) {
+ this.renderer = context.objectInstance.parent.pid;
+ } else {
+ throw new Error('Unknown context type');
+ }
+ this.eventsOfInterest.addEventSet(this.contexts);
+
+ // TODO(xiaochengh): Handle the case where a subframe has a trivial url
+ // (e.g., about:blank), but inherits the origin of its parent. This is not
+ // needed now, but will be required if we want to group rows by origin.
+ this.url = context.url;
+ }
+
+ const groupFunctions = {
+ none: rows => rows,
+
+ // Group the rows according to the frame tree structure.
+ // Example: consider frame tree a(b, c(d)), where each frame has 1ms time
+ // attributed to it. The resulting table should look like:
+ // Type | Time | URL
+ // --------------+------+-----
+ // Frame Tree | 4 | a
+ // +- Frame | 1 | a
+ // +- Subframe | 1 | b
+ // +- Frame Tree | 2 | c
+ // +- Frame | 1 | c
+ // +- Subframe | 1 | d
+ tree(rows, rowMap) {
+ // Finds the parent of a specific row. When there is conflict between the
+ // browser's dump of the frame tree and the renderers', use the browser's.
+ const getParentRow = function(row) {
+ let pivot;
+ row.contexts.forEach(function(context) {
+ if (context instanceof tr.e.chrome.FrameTreeNodeSnapshot) {
+ pivot = context;
+ }
+ });
+ if (pivot && pivot.parentContext) {
+ return rowMap[pivot.parentContext.guid];
+ }
+ return undefined;
+ };
+
+ const rootRows = [];
+ rows.forEach(function(row) {
+ const parentRow = getParentRow(row);
+ if (parentRow === undefined) {
+ rootRows.push(row);
+ return;
+ }
+ if (parentRow.subRows === undefined) {
+ parentRow.subRows = [];
+ }
+ parentRow.subRows.push(row);
+ });
+
+ const aggregateAllDescendants = function(row) {
+ if (!row.subRows) {
+ if (getParentRow(row)) {
+ row.type = 'Subframe';
+ }
+ return row;
+ }
+ const result = new Row();
+ result.type = 'Frame Tree';
+ result.renderer = row.renderer;
+ result.url = row.url;
+ result.subRows = [row];
+ row.subRows.forEach(
+ subRow => result.subRows.push(aggregateAllDescendants(subRow)));
+ result.subRows.forEach(function(subRow) {
+ result.time += subRow.time;
+ result.eventsOfInterest.addEventSet(subRow.eventsOfInterest);
+ });
+ row.subRows = undefined;
+ return result;
+ };
+
+ return rootRows.map(rootRow => aggregateAllDescendants(rootRow));
+ }
+
+ // TODO(xiaochengh): Add grouping by site and probably more...
+ };
+
+ Polymer({
+ is: 'tr-ui-e-s-frame-data-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+ ready() {
+ this.model_ = undefined;
+ this.rangeOfInterest_ = new tr.b.math.Range();
+
+ this.$.table.showHeader = true;
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.table.tableColumns = this.createFrameDataTableColumns_();
+
+ this.$.table.addEventListener('selection-changed', function(e) {
+ this.selectEventSet_(this.$.table.selectedTableRow.eventsOfInterest);
+ }.bind(this));
+
+ this.$.select.addEventListener('change', function(e) {
+ this.updateContents_();
+ }.bind(this));
+ },
+
+ selectEventSet_(eventSet) {
+ const event = new tr.model.RequestSelectionChangeEvent();
+ event.selection = eventSet;
+ this.dispatchEvent(event);
+ },
+
+ createFrameDataTableColumns_() {
+ return [
+ {
+ title: 'Renderer',
+ value: row => row.renderer,
+ cmp: (a, b) => a.renderer - b.renderer
+ },
+ {
+ title: 'Type',
+ value: row => row.type
+ },
+ // TODO(xiaochengh): Decide what details to show in the table:
+ // - URL seems necessary, but we may also want origin instead/both.
+ // - Distinguish between browser time and renderer time?
+ // - Distinguish between CPU time and wall clock time?
+ // - Memory? Network? ...
+ {
+ title: 'Time',
+ value: row => tr.v.ui.createScalarSpan(row.time, {
+ unit: tr.b.Unit.byName.timeStampInMs,
+ ownerDocument: this.ownerDocument
+ }),
+ cmp: (a, b) => a.time - b.time
+ },
+ {
+ title: 'URL',
+ value: row => row.url,
+ cmp: (a, b) => (a.url || '').localeCompare(b.url || '')
+ }
+ ];
+ },
+
+ createFrameDataTableRows_() {
+ if (!this.model_) return [];
+
+ // Gather contexts into skeletons of rows.
+ const rows = [];
+ const rowMap = {};
+ for (const proc of Object.values(this.model_.processes)) {
+ proc.objects.iterObjectInstances(function(objectInstance) {
+ if (!(objectInstance instanceof BlameContextInstance)) {
+ return;
+ }
+ objectInstance.snapshots.forEach(function(snapshot) {
+ if (rowMap[snapshot.guid]) return;
+
+ const row = new Row(snapshot);
+ row.contexts.forEach(context => rowMap[context.guid] = row);
+ rows.push(row);
+ }, this);
+ }, this);
+ }
+
+ // Find slices attributed to each row.
+ // TODO(xiaochengh): We should implement a getter
+ // BlameContextSnapshot.attributedEvents, instead of process the model in
+ // a UI component.
+ for (const proc of Object.values(this.model_.processes)) {
+ for (const thread of Object.values(proc.threads)) {
+ thread.sliceGroup.iterSlicesInTimeRange(function(topLevelSlice) {
+ topLevelSlice.contexts.forEach(function(context) {
+ if (!context.snapshot.guid || !rowMap[context.snapshot.guid]) {
+ return;
+ }
+ const row = rowMap[context.snapshot.guid];
+ row.eventsOfInterest.push(topLevelSlice);
+ row.time += topLevelSlice.selfTime || 0;
+ });
+ }, this.currentRangeOfInterest.min, this.currentRangeOfInterest.max);
+ }
+ }
+
+ // Apply grouping to rows.
+ const select = this.$.select;
+ const groupOption = select.options[select.selectedIndex].value;
+ const groupFunction = groupFunctions[groupOption];
+ return groupFunction(rows, rowMap);
+ },
+
+ updateContents_() {
+ this.$.table.tableRows = this.createFrameDataTableRows_();
+ this.$.table.rebuild();
+ },
+
+ supportsModel(m) {
+ if (!m) {
+ return {
+ supported: false,
+ reason: 'No model available.'
+ };
+ }
+
+ const ans = {supported: false};
+ for (const proc of Object.values(m.processes)) {
+ proc.objects.iterObjectInstances(function(instance) {
+ if (instance instanceof BlameContextInstance) {
+ ans.supported = true;
+ }
+ });
+ }
+
+ if (!ans.supported) {
+ ans.reason = 'No frame data available';
+ }
+ return ans;
+ },
+
+ get currentRangeOfInterest() {
+ if (this.rangeOfInterest_.isEmpty) {
+ return this.model_.bounds;
+ }
+ return this.rangeOfInterest_;
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ this.rangeOfInterest_ = rangeOfInterest;
+ this.updateContents_();
+ },
+
+ get selection() {
+ // Not applicable.
+ },
+
+ set selection(_) {
+ // Not applicable.
+ },
+
+ get textLabel() {
+ return 'Frame Data';
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+ }
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-e-s-frame-data-side-panel');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html
new file mode 100644
index 00000000000..298afe05d42
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html">
+<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/frame_data_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestUtils = tr.c.TestUtils;
+
+ function topLevelOptions(pid, id) {
+ return {
+ pid,
+ id,
+ cat: 'blink',
+ scope: 'PlatformThread',
+ name: 'TopLevel'
+ };
+ }
+
+ function renderFrameOptions(pid, id, parent) {
+ return {
+ pid,
+ id,
+ cat: 'blink',
+ scope: 'RenderFrame',
+ name: 'RenderFrame',
+ args: {parent: {
+ id_ref: parent.id,
+ scope: parent.scope
+ }}
+ };
+ }
+
+ function frameTreeNodeOptions(pid, id, opt_renderFrame, opt_parentId) {
+ const ans = {
+ pid,
+ id,
+ cat: 'navigation',
+ scope: 'FrameTreeNode',
+ name: 'FrameTreeNode',
+ args: {}
+ };
+ if (opt_renderFrame) {
+ ans.args.renderFrame = {
+ id_ref: opt_renderFrame.id,
+ pid_ref: opt_renderFrame.pid,
+ scope: 'RenderFrame'
+ };
+ }
+ if (opt_parentId) {
+ ans.args.parent = {
+ id_ref: opt_parentId,
+ scope: 'FrameTreeNode'
+ };
+ }
+ return ans;
+ }
+
+ /**
+ * Creates some independent contexts. Checks if all are present in the panel.
+ */
+ test('basic', function() {
+ const panel = document.createElement('tr-ui-e-s-frame-data-side-panel');
+ panel.model = TestUtils.newModel(function(model) {
+ TestUtils.newSnapshot(model, topLevelOptions(1, '0x1'));
+ TestUtils.newSnapshot(model, renderFrameOptions(
+ 1, '0x2', {id: '0x1', scope: 'PlatformThread'}));
+ TestUtils.newSnapshot(model, frameTreeNodeOptions(
+ 2, '0x3'));
+ });
+ assert.lengthOf(panel.$.table.tableRows, 3);
+
+ this.addHTMLOutput(panel);
+ });
+
+ /**
+ * Creates a FrameTreeNode in the browser process and a RenderFrame in a
+ * renderer process that are the same frame. Checks if they are merged into
+ * one row in the panel.
+ */
+ test('mergeCrossProcessFrameBlameContexts', function() {
+ const panel = document.createElement('tr-ui-e-s-frame-data-side-panel');
+ panel.model = TestUtils.newModel(function(model) {
+ TestUtils.newSnapshot(model, topLevelOptions(1, '0x1'));
+ TestUtils.newSnapshot(model, renderFrameOptions(
+ 1, '0x2', {id: '0x1', scope: 'PlatformThread'}));
+ TestUtils.newSnapshot(model, frameTreeNodeOptions(
+ 2, '0x3', {id: '0x2', pid: 1}));
+ });
+ assert.lengthOf(panel.$.table.tableRows, 2);
+
+ this.addHTMLOutput(panel);
+ });
+
+ function newAttributedSlice(model, pid, start, duration, context) {
+ const slice = TestUtils.newSliceEx({start, duration});
+ slice.contexts = [{type: 'FrameBlameContext', snapshot: context}];
+ model.getOrCreateProcess(pid).getOrCreateThread(1).sliceGroup.pushSlice(
+ slice);
+ return slice;
+ }
+
+ /**
+ * Changes the range of interest. Checks if the panel updates correspondingly.
+ */
+ test('respondToRangeOfInterest', function() {
+ let topLevel;
+ let slice1;
+ let slice2;
+ const panel = document.createElement('tr-ui-e-s-frame-data-side-panel');
+ panel.model = TestUtils.newModel(function(model) {
+ topLevel = TestUtils.newSnapshot(model, topLevelOptions(1, '0x1'));
+ slice1 = newAttributedSlice(model, 1, 1500, 500, topLevel);
+ slice2 = newAttributedSlice(model, 1, 2500, 500, topLevel);
+ });
+
+ // The default range of interest contains both slices.
+ assert.isTrue(panel.$.table.tableRows[0].eventsOfInterest.equals(
+ new tr.model.EventSet([topLevel, slice1, slice2])));
+
+ // The new range of interest contains only slice2.
+ panel.rangeOfInterest = tr.b.math.Range.fromExplicitRange(slice2.start,
+ slice2.end);
+ assert.isTrue(panel.$.table.tableRows[0].eventsOfInterest.equals(
+ new tr.model.EventSet([topLevel, slice2])));
+
+ this.addHTMLOutput(panel);
+ });
+
+ /**
+ * Selects a row in the panel. Checks if the context(s) of the row and the
+ * slices attributed to the row are selected.
+ */
+ test('selectAttributedEvents', function() {
+ let topLevel;
+ let slice;
+ const panel = document.createElement('tr-ui-e-s-frame-data-side-panel');
+ panel.model = TestUtils.newModel(function(model) {
+ topLevel = TestUtils.newSnapshot(model, topLevelOptions(1, '0x1'));
+ slice = newAttributedSlice(model, 1, 1500, 500, topLevel);
+ });
+
+ let selectionChanged = false;
+ panel.addEventListener('requestSelectionChange', function(e) {
+ selectionChanged = true;
+ assert.isTrue(
+ e.selection.equals(new tr.model.EventSet([topLevel, slice])));
+ });
+ panel.$.table.selectedTableRow = panel.$.table.tableRows[0];
+ assert.isTrue(selectionChanged);
+
+ this.addHTMLOutput(panel);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html
new file mode 100644
index 00000000000..14e33919922
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html
@@ -0,0 +1,334 @@
+<!DOCTYPE html>
+<!--
+Copyright 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/line_chart.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<dom-module id='tr-ui-e-s-input-latency-side-panel'>
+ <template>
+ <style>
+ :host {
+ flex-direction: column;
+ display: flex;
+ }
+ toolbar {
+ flex: 0 0 auto;
+ border-bottom: 1px solid black;
+ display: flex;
+ }
+ result-area {
+ flex: 1 1 auto;
+ display: block;
+ min-height: 0;
+ overflow-y: auto;
+ }
+ </style>
+
+ <toolbar id='toolbar'></toolbar>
+ <result-area id='result_area'></result-area>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-s-input-latency-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+
+ ready() {
+ this.rangeOfInterest_ = new tr.b.math.Range();
+ this.frametimeType_ = tr.model.helpers.IMPL_FRAMETIME_TYPE;
+ this.latencyChart_ = undefined;
+ this.frametimeChart_ = undefined;
+ this.selectedProcessId_ = undefined;
+ this.mouseDownIndex_ = undefined;
+ this.curMouseIndex_ = undefined;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ if (this.model_) {
+ this.modelHelper_ = this.model_.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ } else {
+ this.modelHelper_ = undefined;
+ }
+
+ this.updateToolbar_();
+ this.updateContents_();
+ },
+
+ get frametimeType() {
+ return this.frametimeType_;
+ },
+
+ set frametimeType(type) {
+ if (this.frametimeType_ === type) return;
+
+ this.frametimeType_ = type;
+ this.updateContents_();
+ },
+
+ get selectedProcessId() {
+ return this.selectedProcessId_;
+ },
+
+ set selectedProcessId(process) {
+ if (this.selectedProcessId_ === process) return;
+
+ this.selectedProcessId_ = process;
+ this.updateContents_();
+ },
+
+ set selection(selection) {
+ if (this.latencyChart_ === undefined) return;
+
+ this.latencyChart_.brushedRange = selection.bounds;
+ },
+
+ // This function is for testing purpose.
+ setBrushedIndices(mouseDownIndex, curIndex) {
+ this.mouseDownIndex_ = mouseDownIndex;
+ this.curMouseIndex_ = curIndex;
+ this.updateBrushedRange_();
+ },
+
+ updateBrushedRange_() {
+ if (this.latencyChart_ === undefined) return;
+
+ let r = new tr.b.math.Range();
+ if (this.mouseDownIndex_ === undefined) {
+ this.latencyChart_.brushedRange = r;
+ return;
+ }
+ r = this.latencyChart_.computeBrushRangeFromIndices(
+ this.mouseDownIndex_, this.curMouseIndex_);
+ this.latencyChart_.brushedRange = r;
+
+ // Based on the brushed range, update the selection of LatencyInfo in
+ // the timeline view by sending a selectionChange event.
+ let latencySlices = [];
+ for (const thread of this.model_.getAllThreads()) {
+ for (const event of thread.getDescendantEvents()) {
+ if (event.title.indexOf('InputLatency:') === 0) {
+ latencySlices.push(event);
+ }
+ }
+ }
+ latencySlices = tr.model.helpers.getSlicesIntersectingRange(
+ r, latencySlices);
+
+ const event = new tr.model.RequestSelectionChangeEvent();
+ event.selection = new tr.model.EventSet(latencySlices);
+ this.latencyChart_.dispatchEvent(event);
+ },
+
+ registerMouseEventForLatencyChart_() {
+ this.latencyChart_.addEventListener('item-mousedown', function(e) {
+ this.mouseDownIndex_ = e.index;
+ this.curMouseIndex_ = e.index;
+ this.updateBrushedRange_();
+ }.bind(this));
+
+ this.latencyChart_.addEventListener('item-mousemove', function(e) {
+ if (e.button === undefined) return;
+
+ this.curMouseIndex_ = e.index;
+ this.updateBrushedRange_();
+ }.bind(this));
+
+ this.latencyChart_.addEventListener('item-mouseup', function(e) {
+ this.curMouseIndex = e.index;
+ this.updateBrushedRange_();
+ }.bind(this));
+ },
+
+ updateToolbar_() {
+ const browserProcess = this.modelHelper_.browserProcess;
+ const labels = [];
+
+ if (browserProcess !== undefined) {
+ const labelStr = 'Browser: ' + browserProcess.pid;
+ labels.push({label: labelStr, value: browserProcess.pid});
+ }
+
+ for (const rendererHelper of
+ Object.values(this.modelHelper_.rendererHelpers)) {
+ const rendererProcess = rendererHelper.process;
+ const labelStr = 'Renderer: ' + rendererProcess.userFriendlyName;
+ labels.push({label: labelStr, value: rendererProcess.userFriendlyName});
+ }
+
+ if (labels.length === 0) return;
+
+ this.selectedProcessId_ = labels[0].value;
+ const toolbarEl = this.$.toolbar;
+ Polymer.dom(toolbarEl).appendChild(tr.ui.b.createSelector(
+ this, 'frametimeType',
+ 'inputLatencySidePanel.frametimeType', this.frametimeType_,
+ [{label: 'Main Thread Frame Times',
+ value: tr.model.helpers.MAIN_FRAMETIME_TYPE},
+ {label: 'Impl Thread Frame Times',
+ value: tr.model.helpers.IMPL_FRAMETIME_TYPE}
+ ]));
+ Polymer.dom(toolbarEl).appendChild(tr.ui.b.createSelector(
+ this, 'selectedProcessId',
+ 'inputLatencySidePanel.selectedProcessId',
+ this.selectedProcessId_,
+ labels));
+ },
+
+ // TODO(charliea): Delete this function in favor of rangeOfInterest.
+ get currentRangeOfInterest() {
+ if (this.rangeOfInterest_.isEmpty) {
+ return this.model_.bounds;
+ }
+ return this.rangeOfInterest_;
+ },
+
+ createLatencyLineChart(data, title, parentNode) {
+ const chart = new tr.ui.b.LineChart();
+ Polymer.dom(parentNode).appendChild(chart);
+ let width = 600;
+ if (document.body.clientWidth !== undefined) {
+ width = document.body.clientWidth * 0.5;
+ }
+ chart.graphWidth = width;
+ chart.chartTitle = title;
+ chart.data = data;
+ return chart;
+ },
+
+ updateContents_() {
+ const resultArea = this.$.result_area;
+ this.latencyChart_ = undefined;
+ this.frametimeChart_ = undefined;
+ Polymer.dom(resultArea).textContent = '';
+
+ if (this.modelHelper_ === undefined) return;
+
+ const rangeOfInterest = this.currentRangeOfInterest;
+
+ let chromeProcess;
+ if (this.modelHelper_.rendererHelpers[this.selectedProcessId_]) {
+ chromeProcess = this.modelHelper_.rendererHelpers[
+ this.selectedProcessId_
+ ];
+ } else {
+ chromeProcess = this.modelHelper_.browserHelper;
+ }
+
+ const frameEvents = chromeProcess.getFrameEventsInRange(
+ this.frametimeType, rangeOfInterest);
+
+ const frametimeData = tr.model.helpers.getFrametimeDataFromEvents(
+ frameEvents);
+ const averageFrametime = tr.b.math.Statistics.mean(frametimeData, d =>
+ d.frametime
+ );
+
+ const latencyEvents = this.modelHelper_.browserHelper.
+ getLatencyEventsInRange(
+ rangeOfInterest);
+
+ const latencyData = [];
+ latencyEvents.forEach(function(event) {
+ if (event.inputLatency === undefined) return;
+
+ latencyData.push({
+ x: event.start,
+ latency: event.inputLatency / 1000
+ });
+ });
+
+ const averageLatency = tr.b.math.Statistics.mean(latencyData, function(d) {
+ return d.latency;
+ });
+
+ // Create summary.
+ const latencySummaryText = document.createElement('div');
+ Polymer.dom(latencySummaryText).appendChild(tr.ui.b.createSpan({
+ textContent: 'Average Latency ' + averageLatency + ' ms',
+ bold: true}));
+ Polymer.dom(resultArea).appendChild(latencySummaryText);
+
+ const frametimeSummaryText = document.createElement('div');
+ Polymer.dom(frametimeSummaryText).appendChild(tr.ui.b.createSpan({
+ textContent: 'Average Frame Time ' + averageFrametime + ' ms',
+ bold: true}));
+ Polymer.dom(resultArea).appendChild(frametimeSummaryText);
+
+ if (latencyData.length !== 0) {
+ this.latencyChart_ = this.createLatencyLineChart(
+ latencyData, 'Latency Over Time', resultArea);
+ this.registerMouseEventForLatencyChart_();
+ }
+
+ if (frametimeData.length !== 0) {
+ this.frametimeChart_ = this.createLatencyLineChart(
+ frametimeData, 'Frame Times', resultArea);
+ }
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ this.rangeOfInterest_ = rangeOfInterest;
+ this.updateContents_();
+ },
+
+ supportsModel(m) {
+ if (m === undefined) {
+ return {
+ supported: false,
+ reason: 'Unknown tracing model'
+ };
+ }
+
+ if (!tr.model.helpers.ChromeModelHelper.supportsModel(m)) {
+ return {
+ supported: false,
+ reason: 'No Chrome browser or renderer process found'
+ };
+ }
+
+ const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
+ if (modelHelper.browserHelper &&
+ modelHelper.browserHelper.hasLatencyEvents) {
+ return {
+ supported: true
+ };
+ }
+
+ return {
+ supported: false,
+ reason: 'No InputLatency events trace. Consider enabling ' +
+ 'benchmark" and "input" category when recording the trace'
+ };
+ },
+
+ get textLabel() {
+ return 'Input Latency';
+ }
+});
+
+tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-e-s-input-latency-side-panel');
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html
new file mode 100644
index 00000000000..de225416faa
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/input_latency_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('basic', function() {
+ const latencyData = [
+ {
+ x: 1000,
+ latency: 16
+ },
+ {
+ x: 2000,
+ latency: 17
+ },
+ {
+ x: 3000,
+ latency: 14
+ },
+ {
+ x: 4000,
+ latency: 23
+ }
+ ];
+ let lc = document.createElement('tr-ui-e-s-input-latency-side-panel');
+ let container = document.createElement('div');
+ this.addHTMLOutput(container);
+ const latencyChart = lc.createLatencyLineChart(
+ latencyData, 'latency', container);
+
+ const frametimeData = [
+ {
+ x: 1000,
+ frametime: 16
+ },
+ {
+ x: 2000,
+ frametime: 17
+ },
+ {
+ x: 3000,
+ frametime: 14
+ },
+ {
+ x: 4000,
+ frametime: 23
+ }
+ ];
+ lc = document.createElement('tr-ui-e-s-input-latency-side-panel');
+ container = document.createElement('div');
+ this.addHTMLOutput(container);
+ const frametimeChart = lc.createLatencyLineChart(
+ frametimeData, 'frametime', container);
+ });
+
+ test('brushedRangeChange', function() {
+ const events = [];
+ for (let i = 0; i < 10; i++) {
+ const startTs = i * 10000;
+ const endTs = startTs + 1000 * (i % 2);
+ events.push(
+ {
+ 'cat': 'benchmark',
+ 'pid': 3507,
+ 'tid': 3507,
+ 'ts': startTs,
+ 'ph': 'S',
+ 'name': 'InputLatency',
+ 'id': i
+ });
+ events.push(
+ {
+ 'cat': 'benchmark',
+ 'pid': 3507,
+ 'tid': 3507,
+ 'ts': endTs,
+ 'ph': 'T',
+ 'name': 'InputLatency',
+ 'args': {'step': 'GestureScrollUpdate'},
+ 'id': i
+ });
+ events.push(
+ {
+ 'cat': 'benchmark',
+ 'pid': 3507,
+ 'tid': 3507,
+ 'ts': endTs,
+ 'ph': 'F',
+ 'name': 'InputLatency',
+ 'args': {
+ 'data': {
+ 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT': {
+ 'time': startTs
+ },
+ 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT': {
+ 'time': endTs
+ }
+ }
+ },
+ 'id': i
+ });
+ }
+ events.push({'cat': '__metadata',
+ 'pid': 3507,
+ 'tid': 3507,
+ 'ts': 0,
+ 'ph': 'M',
+ 'name': 'thread_name',
+ 'args': {'name': 'CrBrowserMain'}});
+
+ const panel = document.createElement('tr-ui-e-s-input-latency-side-panel');
+ this.addHTMLOutput(panel);
+
+ let selectionChanged = false;
+
+ panel.model = tr.c.TestUtils.newModelWithEvents([events]);
+ function listener(e) {
+ selectionChanged = true;
+ assert.strictEqual(e.selection.length, 3);
+ const predictedStarts = [20, 31, 40];
+ let i = 0;
+ for (const event of e.selection) {
+ assert.strictEqual(event.start, predictedStarts[i++]);
+ }
+ }
+ panel.ownerDocument.addEventListener('requestSelectionChange', listener);
+ try {
+ panel.setBrushedIndices(2, 4);
+ } finally {
+ panel.ownerDocument.removeEventListener(
+ 'requestSelectionChange', listener);
+ }
+ assert.isTrue(selectionChanged);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html
new file mode 100644
index 00000000000..31bc1dbd997
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/system_stats/system_stats_snapshot.html">
+<link rel="import"
+ href="/tracing/ui/extras/system_stats/system_stats_instance_track.html">
+<link rel="import"
+ href="/tracing/ui/extras/system_stats/system_stats_snapshot_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css
new file mode 100644
index 00000000000..40096f5497c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css
@@ -0,0 +1,15 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.tr-ui-e-system-stats-instance-track {
+ height: 500px;
+}
+
+.tr-ui-e-system-stats-instance-track ul {
+ list-style: none;
+ list-style-position: outside;
+ margin: 0;
+ overflow: hidden;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html
new file mode 100644
index 00000000000..7695086660c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html
@@ -0,0 +1,451 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet"
+ href="/tracing/ui/extras/system_stats/system_stats_instance_track.css">
+
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_track.html">
+<link rel="import" href="/tracing/ui/tracks/stacked_bars_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.system_stats', function() {
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ let statCount;
+
+ const excludedStats = {'meminfo': {
+ 'pswpin': 0,
+ 'pswpout': 0,
+ 'pgmajfault': 0},
+ 'diskinfo': {
+ 'io': 0,
+ 'io_time': 0,
+ 'read_time': 0,
+ 'reads': 0,
+ 'reads_merged': 0,
+ 'sectors_read': 0,
+ 'sectors_written': 0,
+ 'weighted_io_time': 0,
+ 'write_time': 0,
+ 'writes': 0,
+ 'writes_merged': 0},
+ 'swapinfo': {},
+ 'perfinfo': {
+ 'idle_time': 0,
+ 'read_transfer_count': 0,
+ 'write_transfer_count': 0,
+ 'other_transfer_count': 0,
+ 'read_operation_count': 0,
+ 'write_operation_count': 0,
+ 'other_operation_count': 0,
+ 'pagefile_pages_written': 0,
+ 'pagefile_pages_write_ios': 0,
+ 'available_pages': 0,
+ 'pages_read': 0,
+ 'page_read_ios': 0}
+ };
+
+ /**
+ * Tracks that display system stats data.
+ *
+ * @constructor
+ * @extends {StackedBarsTrack}
+ */
+
+ const SystemStatsInstanceTrack = tr.ui.b.define(
+ 'tr-ui-e-system-stats-instance-track', tr.ui.tracks.StackedBarsTrack);
+
+ const kPageSizeWindows = 4096;
+
+ SystemStatsInstanceTrack.prototype = {
+
+ __proto__: tr.ui.tracks.StackedBarsTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.StackedBarsTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('tr-ui-e-system-stats-instance-track');
+ this.objectInstance_ = null;
+ },
+
+ set objectInstances(objectInstances) {
+ if (!objectInstances) {
+ this.objectInstance_ = [];
+ return;
+ }
+ if (objectInstances.length !== 1) {
+ throw new Error('Bad object instance count.');
+ }
+ this.objectInstance_ = objectInstances[0];
+ if (this.objectInstance_ !== null) {
+ this.computeRates_(this.objectInstance_.snapshots);
+ this.maxStats_ = this.computeMaxStats_(
+ this.objectInstance_.snapshots);
+ }
+ },
+
+ computeRates_(snapshots) {
+ for (let i = 0; i < snapshots.length; i++) {
+ const snapshot = snapshots[i];
+ const stats = snapshot.getStats();
+ let prevSnapshot;
+
+ if (i === 0) {
+ // Deltas will be zero.
+ prevSnapshot = snapshots[0];
+ } else {
+ prevSnapshot = snapshots[i - 1];
+ }
+ const prevStats = prevSnapshot.getStats();
+ let timeIntervalSeconds = (snapshot.ts - prevSnapshot.ts) / 1000;
+ // Prevent divide by zero.
+ if (timeIntervalSeconds === 0) {
+ timeIntervalSeconds = 1;
+ }
+
+ this.computeRatesRecursive_(prevStats, stats,
+ timeIntervalSeconds);
+ }
+ },
+
+ computeRatesRecursive_(prevStats, stats,
+ timeIntervalSeconds) {
+ for (const statName in stats) {
+ if (stats[statName] instanceof Object) {
+ this.computeRatesRecursive_(prevStats[statName],
+ stats[statName],
+ timeIntervalSeconds);
+ } else {
+ if (statName === 'sectors_read') {
+ stats.bytes_read_per_sec = (stats.sectors_read -
+ prevStats.sectors_read) *
+ 512 / timeIntervalSeconds;
+ }
+ if (statName === 'sectors_written') {
+ stats.bytes_written_per_sec =
+ (stats.sectors_written -
+ prevStats.sectors_written) *
+ 512 / timeIntervalSeconds;
+ }
+ if (statName === 'pgmajfault') {
+ stats.pgmajfault_per_sec = (stats.pgmajfault -
+ prevStats.pgmajfault) /
+ timeIntervalSeconds;
+ }
+ if (statName === 'pswpin') {
+ stats.bytes_swpin_per_sec = (stats.pswpin -
+ prevStats.pswpin) *
+ 1000 / timeIntervalSeconds;
+ }
+ if (statName === 'pswpout') {
+ stats.bytes_swpout_per_sec = (stats.pswpout -
+ prevStats.pswpout) *
+ 1000 / timeIntervalSeconds;
+ }
+
+ // All the stats below are available only on Windows:
+
+ if (statName === 'idle_time') {
+ // Total amount of idle_time, in unit of 100 nanoseconds.
+ const units = tr.b.convertUnit(100.,
+ tr.b.UnitScale.TIME.NANO_SEC, tr.b.UnitScale.TIME.SEC);
+ const idleTile = (stats.idle_time - prevStats.idle_time) * units;
+ stats.idle_time_per_sec = idleTile / timeIntervalSeconds;
+ }
+ if (statName === 'read_transfer_count') {
+ const bytesRead = stats.read_transfer_count -
+ prevStats.read_transfer_count;
+ stats.bytes_read_per_sec = bytesRead / timeIntervalSeconds;
+ }
+ if (statName === 'write_transfer_count') {
+ const bytesWritten = stats.write_transfer_count -
+ prevStats.write_transfer_count;
+ stats.bytes_written_per_sec = bytesWritten / timeIntervalSeconds;
+ }
+ if (statName === 'other_transfer_count') {
+ const bytesTransfer = stats.other_transfer_count -
+ prevStats.other_transfer_count;
+ stats.bytes_other_per_sec = bytesTransfer / timeIntervalSeconds;
+ }
+ if (statName === 'read_operation_count') {
+ const readOperation = stats.read_operation_count -
+ prevStats.read_operation_count;
+ stats.read_operation_per_sec = readOperation / timeIntervalSeconds;
+ }
+ if (statName === 'write_operation_count') {
+ const writeOperation = stats.write_operation_count -
+ prevStats.write_operation_count;
+ stats.write_operation_per_sec =
+ writeOperation / timeIntervalSeconds;
+ }
+ if (statName === 'other_operation_count') {
+ const otherOperation = stats.other_operation_count -
+ prevStats.other_operation_count;
+ stats.other_operation_per_sec =
+ otherOperation / timeIntervalSeconds;
+ }
+ if (statName === 'pagefile_pages_written') {
+ const pageFileBytesWritten =
+ (stats.pagefile_pages_written -
+ prevStats.pagefile_pages_written) * kPageSizeWindows;
+ stats.pagefile_bytes_written_per_sec =
+ pageFileBytesWritten / timeIntervalSeconds;
+ }
+ if (statName === 'pagefile_pages_write_ios') {
+ const pagefileWriteOperation =
+ stats.pagefile_pages_write_ios -
+ prevStats.pagefile_pages_write_ios;
+ stats.pagefile_write_operation_per_sec =
+ pagefileWriteOperation / timeIntervalSeconds;
+ }
+ if (statName === 'available_pages') {
+ // Nothing to do here for now.
+ stats.available_pages_in_bytes =
+ stats.available_pages * kPageSizeWindows;
+ // TODO(sebmarchand): Add a available_pages_field that tracks the
+ // variation of this metric?
+ }
+ if (statName === 'pages_read') {
+ const pagesBytesRead =
+ (stats.pages_read - prevStats.pages_read) * kPageSizeWindows;
+ stats.bytes_read_per_sec = pagesBytesRead / timeIntervalSeconds;
+ }
+ if (statName === 'page_read_ios') {
+ const pagesBytesReadOperations =
+ stats.page_read_ios - prevStats.page_read_ios;
+ stats.pagefile_write_operation_per_sec =
+ pagesBytesReadOperations / timeIntervalSeconds;
+ }
+ }
+ }
+ },
+
+ computeMaxStats_(snapshots) {
+ const maxStats = {};
+ statCount = 0;
+
+ for (let i = 0; i < snapshots.length; i++) {
+ const snapshot = snapshots[i];
+ const stats = snapshot.getStats();
+
+ this.computeMaxStatsRecursive_(stats, maxStats,
+ excludedStats);
+ }
+
+ return maxStats;
+ },
+
+ computeMaxStatsRecursive_(stats, maxStats, excludedStats) {
+ for (const statName in stats) {
+ if (stats[statName] instanceof Object) {
+ if (!(statName in maxStats)) {
+ maxStats[statName] = {};
+ }
+
+ let excludedNested;
+ if (excludedStats && statName in excludedStats) {
+ excludedNested = excludedStats[statName];
+ } else {
+ excludedNested = null;
+ }
+
+ this.computeMaxStatsRecursive_(stats[statName],
+ maxStats[statName],
+ excludedNested);
+ } else {
+ if (excludedStats && statName in excludedStats) {
+ continue;
+ }
+ if (!(statName in maxStats)) {
+ maxStats[statName] = 0;
+ statCount++;
+ }
+ if (stats[statName] > maxStats[statName]) {
+ maxStats[statName] = stats[statName];
+ }
+ }
+ }
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawStatBars_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawStatBars_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const bounds = this.getBoundingClientRect();
+ const width = bounds.width * pixelRatio;
+ const height = (bounds.height * pixelRatio) / statCount;
+
+ // Culling parameters.
+ const vp = this.viewport.currentDisplayTransform;
+
+ // Scale by the size of the largest snapshot.
+ const maxStats = this.maxStats_;
+
+ const objectSnapshots = this.objectInstance_.snapshots;
+ let lowIndex = tr.b.findLowIndexInSortedArray(
+ objectSnapshots,
+ function(snapshot) {
+ return snapshot.ts;
+ },
+ viewLWorld);
+
+ // Assure that the stack with the left edge off screen still gets drawn
+ if (lowIndex > 0) lowIndex -= 1;
+
+ for (let i = lowIndex; i < objectSnapshots.length; ++i) {
+ const snapshot = objectSnapshots[i];
+ const trace = snapshot.getStats();
+ const currentY = height;
+
+ const left = snapshot.ts;
+ if (left > viewRWorld) break;
+
+ let leftView = vp.xWorldToView(left);
+ if (leftView < 0) leftView = 0;
+
+ // Compute the edges for the column graph bar.
+ let right;
+ if (i !== objectSnapshots.length - 1) {
+ right = objectSnapshots[i + 1].ts;
+ } else {
+ // If this is the last snapshot of multiple snapshots, use the width
+ // of the previous snapshot for the width.
+ if (objectSnapshots.length > 1) {
+ right = objectSnapshots[i].ts + (objectSnapshots[i].ts -
+ objectSnapshots[i - 1].ts);
+ } else {
+ // If there's only one snapshot, use max bounds as the width.
+ right = this.objectInstance_.parent.model.bounds.max;
+ }
+ }
+
+ let rightView = vp.xWorldToView(right);
+ if (rightView > width) {
+ rightView = width;
+ }
+
+ // Floor the bounds to avoid a small gap between stacks.
+ leftView = Math.floor(leftView);
+ rightView = Math.floor(rightView);
+
+ // Descend into nested stats.
+ this.drawStatBarsRecursive_(snapshot,
+ leftView,
+ rightView,
+ height,
+ trace,
+ maxStats,
+ currentY);
+
+ if (i === lowIndex) {
+ this.drawStatNames_(leftView, height, currentY, '', maxStats);
+ }
+ }
+ ctx.lineWidth = 1;
+ },
+
+ drawStatBarsRecursive_(snapshot,
+ leftView,
+ rightView,
+ height,
+ stats,
+ maxStats,
+ currentY) {
+ const ctx = this.context();
+
+ for (const statName in maxStats) {
+ if (stats[statName] instanceof Object) {
+ // Use the y-position returned from the recursive call.
+ currentY = this.drawStatBarsRecursive_(snapshot,
+ leftView,
+ rightView,
+ height,
+ stats[statName],
+ maxStats[statName],
+ currentY);
+ } else {
+ const maxStat = maxStats[statName];
+
+ // Draw a bar for the stat. The height of the bar is scaled
+ // against the largest value of the stat across all snapshots.
+ ctx.fillStyle = EventPresenter.getBarSnapshotColor(
+ snapshot, Math.round(currentY / height));
+
+ let barHeight;
+ if (maxStat > 0) {
+ barHeight = height * Math.max(stats[statName], 0) / maxStat;
+ } else {
+ barHeight = 0;
+ }
+
+ ctx.fillRect(leftView, currentY - barHeight,
+ Math.max(rightView - leftView, 1), barHeight);
+
+ currentY += height;
+ }
+ }
+
+ // Return the updated y-position.
+ return currentY;
+ },
+
+ drawStatNames_(leftView, height, currentY, prefix, maxStats) {
+ const ctx = this.context();
+
+ ctx.textAlign = 'end';
+ ctx.font = '12px Arial';
+ ctx.fillStyle = '#000000';
+ for (const statName in maxStats) {
+ if (maxStats[statName] instanceof Object) {
+ currentY = this.drawStatNames_(leftView, height, currentY,
+ statName, maxStats[statName]);
+ } else {
+ let fullname = statName;
+
+ if (prefix !== '') {
+ fullname = prefix + ' :: ' + statName;
+ }
+
+ ctx.fillText(fullname, leftView - 10, currentY - height / 4);
+ currentY += height;
+ }
+ }
+
+ return currentY;
+ }
+ };
+
+ tr.ui.tracks.ObjectInstanceTrack.register(
+ SystemStatsInstanceTrack,
+ {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'});
+
+ return {
+ SystemStatsInstanceTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html
new file mode 100644
index 00000000000..8dc4bc28264
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/extras/system_stats/system_stats.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SystemStatsInstanceTrack =
+ tr.ui.e.system_stats.SystemStatsInstanceTrack;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createObjects = function() {
+ const objectInstance = new tr.model.ObjectInstance({});
+ const snapshots = [];
+
+ const stats1 = {};
+ const stats2 = {};
+
+ stats1.committed_memory = 2000000;
+ stats2.committed_memory = 3000000;
+
+ stats1.meminfo = {};
+ stats1.meminfo.free = 10000;
+ stats2.meminfo = {};
+ stats2.meminfo.free = 20000;
+
+ stats1.perfinfo = {};
+ stats1.perfinfo.idle_time = 10;
+ stats1.perfinfo.read_transfer_count = 20;
+ stats1.perfinfo.write_transfer_count = 30;
+ stats1.perfinfo.other_transfer_count = 40;
+ stats1.perfinfo.read_operation_count = 2;
+ stats1.perfinfo.write_operation_count = 3;
+ stats1.perfinfo.other_operation_count = 4;
+ stats1.perfinfo.pagefile_pages_written = 5;
+ stats1.perfinfo.pagefile_pages_write_ios = 6;
+
+ stats2.perfinfo = {};
+ stats2.perfinfo.idle_time = 110;
+ stats2.perfinfo.read_transfer_count = 120;
+ stats2.perfinfo.write_transfer_count = 130;
+ stats2.perfinfo.other_transfer_count = 140;
+ stats2.perfinfo.read_operation_count = 102;
+ stats2.perfinfo.write_operation_count = 103;
+ stats2.perfinfo.other_operation_count = 104;
+ stats2.perfinfo.pagefile_pages_written = 105;
+ stats2.perfinfo.pagefile_pages_write_ios = 106;
+
+ snapshots.push(new tr.e.system_stats.SystemStatsSnapshot(objectInstance,
+ 10, stats1));
+ snapshots.push(new tr.e.system_stats.SystemStatsSnapshot(objectInstance,
+ 20, stats2));
+
+ objectInstance.snapshots = snapshots;
+
+ return objectInstance;
+ };
+
+ test('instantiate', function() {
+ const objectInstances = [];
+ objectInstances.push(createObjects());
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new SystemStatsInstanceTrack(viewport);
+ track.objectInstances = objectInstances;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ const snapshot1 = track.objectInstance_.snapshots[1];
+ const stats1 = snapshot1.getStats();
+
+ // Raw counters should not move.
+ assert.strictEqual(stats1.perfinfo.idle_time, 110);
+ assert.strictEqual(stats1.perfinfo.read_operation_count, 102);
+ assert.strictEqual(stats1.perfinfo.write_operation_count, 103);
+ assert.strictEqual(stats1.perfinfo.other_operation_count, 104);
+ assert.strictEqual(stats1.perfinfo.read_transfer_count, 120);
+ assert.strictEqual(stats1.perfinfo.write_transfer_count, 130);
+ assert.strictEqual(stats1.perfinfo.other_transfer_count, 140);
+ assert.strictEqual(stats1.perfinfo.pagefile_pages_written, 105);
+ assert.strictEqual(stats1.perfinfo.pagefile_pages_write_ios, 106);
+
+ // Rates should be computed.
+ assert.strictEqual(stats1.perfinfo.idle_time_per_sec, 0.001);
+ assert.strictEqual(stats1.perfinfo.bytes_read_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.bytes_written_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.bytes_other_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.read_operation_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.write_operation_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.other_operation_per_sec, 10000);
+ assert.strictEqual(stats1.perfinfo.pagefile_bytes_written_per_sec,
+ 40960000);
+ assert.strictEqual(stats1.perfinfo.pagefile_write_operation_per_sec, 10000);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasic';
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css
new file mode 100644
index 00000000000..e698b15aa70
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css
@@ -0,0 +1,28 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.tr-ui-e-system-stats-snapshot-view .subhead {
+ font-size: small;
+ padding-bottom: 10px;
+}
+
+.tr-ui-e-system-stats-snapshot-view ul {
+ background-position: 0 5px;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ font-family: monospace;
+ list-style: none;
+ margin: 0;
+ padding-left: 15px;
+}
+
+.tr-ui-e-system-stats-snapshot-view li {
+ background-position: 0 5px;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ list-style: none;
+ margin: 0;
+ padding-left: 15px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html
new file mode 100644
index 00000000000..54f4b869f4a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet"
+ href="/tracing/ui/extras/system_stats/system_stats_snapshot_view.css">
+
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.system_stats', function() {
+ /*
+ * Displays a system stats snapshot in a human readable form. @constructor
+ */
+ const SystemStatsSnapshotView = tr.ui.b.define(
+ 'tr-ui-e-system-stats-snapshot-view', tr.ui.analysis.ObjectSnapshotView);
+
+ SystemStatsSnapshotView.prototype = {
+ __proto__: tr.ui.analysis.ObjectSnapshotView.prototype,
+
+ decorate() {
+ Polymer.dom(this).classList.add('tr-ui-e-system-stats-snapshot-view');
+ },
+
+ updateContents() {
+ const snapshot = this.objectSnapshot_;
+ if (!snapshot || !snapshot.getStats()) {
+ Polymer.dom(this).textContent = 'No system stats snapshot found.';
+ return;
+ }
+ // Clear old snapshot view.
+ Polymer.dom(this).textContent = '';
+
+ const stats = snapshot.getStats();
+ Polymer.dom(this).appendChild(this.buildList_(stats));
+ },
+
+ isFloat(n) {
+ return typeof n === 'number' && n % 1 !== 0;
+ },
+
+ /**
+ * Creates nested lists.
+ *
+ * @param {Object} stats The current trace system stats entry.
+ * @return {Element} A ul list element.
+ */
+ buildList_(stats) {
+ const statList = document.createElement('ul');
+
+ for (const statName in stats) {
+ const statText = document.createElement('li');
+ Polymer.dom(statText).textContent = '' + statName + ': ';
+ Polymer.dom(statList).appendChild(statText);
+
+ if (stats[statName] instanceof Object) {
+ Polymer.dom(statList).appendChild(this.buildList_(stats[statName]));
+ } else {
+ if (this.isFloat(stats[statName])) {
+ Polymer.dom(statText).textContent += stats[statName].toFixed(2);
+ } else {
+ Polymer.dom(statText).textContent += stats[statName];
+ }
+ }
+ }
+
+ return statList;
+ }
+ };
+
+ tr.ui.analysis.ObjectSnapshotView.register(
+ SystemStatsSnapshotView,
+ {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'});
+
+ return {
+ SystemStatsSnapshotView,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html
new file mode 100644
index 00000000000..fcc410b754a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<!--
+TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the
+same way that all non-UI files depend on tracing/base/base.html. Enforce this
+dependency with a presubmit.
+-->
+<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order>
+
+<link rel="import" href="/tracing/extras/systrace_config.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/extras/side_panel/alerts_side_panel.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html
new file mode 100644
index 00000000000..bc3247d9289
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html
@@ -0,0 +1,728 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-e-v8-gc-objects-stats-table'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ flex: 0 0 auto;
+ align-self: stretch;
+ margin-top: 1em;
+ font-size: 12px;
+ }
+ .diff {
+ display: inline-block;
+ margin-top: 1em;
+ margin-left: 0.8em;
+ }
+ </style>
+ <div class="diff" id="diffOption">
+ Diff
+ </div>
+ <tr-ui-b-table id="diffTable"></tr-ui-b-table>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.v8', function() {
+ // Instance types that should not be part of the overview as they are either
+ // double-attributed (e.g. also part of some other instance type) or do not
+ // make any sense in memory profiling.
+ const IGNORED_ENTRIES = {
+ // Ignore code aging entries as they are already accounted in their
+ // respective code instance types.
+ match: full => full.startsWith('*CODE_AGE_')
+ };
+
+ // Groups are matched on a first-matched basis, i.e., once a group matches we
+ // are done with an entry.
+ // Requires properties:
+ // - match(full): Return true iff |full| should be part of the group and
+ // false otherwise.
+ // - keyToName(key): Returns the human readable name for |key|.
+ // - nameToKey(name): Returns the key for |name|.
+ // Optional properties:
+ // - realEntry: A string representing the actual entry in the trace. If this
+ // entry is present an additional entry UNKNOWN will be created holding all
+ // the unaccounted data.
+ const INSTANCE_TYPE_GROUPS = {
+ FIXED_ARRAY_TYPE: {
+ match: full => full.startsWith('*FIXED_ARRAY_'),
+ realEntry: 'FIXED_ARRAY_TYPE',
+ keyToName: key => key.slice('*FIXED_ARRAY_'.length)
+ .slice(0, -('_SUB_TYPE'.length)),
+ nameToKey: name => '*FIXED_ARRAY_' + name + '_SUB_TYPE'
+ },
+ CODE_TYPE: {
+ match: full => full.startsWith('*CODE_'),
+ realEntry: 'CODE_TYPE',
+ keyToName: key => key.slice('*CODE_'.length),
+ nameToKey: name => '*CODE_' + name
+ },
+ JS_OBJECTS: {
+ match: full => full.startsWith('JS_'),
+ keyToName: key => key,
+ nameToKey: name => name
+ },
+ Strings: {
+ match: full => full.endsWith('STRING_TYPE'),
+ keyToName: key => key,
+ nameToKey: name => name
+ }
+ };
+
+ const DIFF_COLOR = {
+ GREEN: '#64DD17',
+ RED: '#D50000'
+ };
+
+ function computePercentage(valueA, valueB) {
+ if (valueA === 0) return 0;
+ return valueA / valueB * 100;
+ }
+
+ class DiffEntry {
+ constructor(originalEntry, diffEntry) {
+ this.originalEntry_ = originalEntry;
+ this.diffEntry_ = diffEntry;
+ }
+ get title() {
+ return this.diffEntry_.title;
+ }
+ get overall() {
+ return this.diffEntry_.overall;
+ }
+ get overAllocated() {
+ return this.diffEntry_.overAllocated;
+ }
+ get count() {
+ return this.diffEntry_.count;
+ }
+ get overallPercent() {
+ return this.diffEntry_.overallPercent;
+ }
+ get overAllocatedPercent() {
+ return this.diffEntry_.overAllocatedPercent;
+ }
+ get origin() {
+ return this.originalEntry_;
+ }
+ get diff() {
+ return this.diffEntry_;
+ }
+ get subRows() {
+ return this.diffEntry_.subRows;
+ }
+ }
+
+ class Entry {
+ constructor(title, count, overall, overAllocated, histogram,
+ overAllocatedHistogram) {
+ this.title_ = title;
+ this.overall_ = overall;
+ this.count_ = count;
+ this.overAllocated_ = overAllocated;
+ this.histogram_ = histogram;
+ this.overAllocatedHistogram_ = overAllocatedHistogram;
+ this.bucketSize_ = this.histogram_.length;
+ this.overallPercent_ = 100;
+ this.overAllocatedPercent_ = 100;
+ }
+
+ get title() {
+ return this.title_;
+ }
+
+ get overall() {
+ return this.overall_;
+ }
+
+ get count() {
+ return this.count_;
+ }
+
+ get overAllocated() {
+ return this.overAllocated_;
+ }
+
+ get histogram() {
+ return this.histogram_;
+ }
+
+ get overAllocatedHistogram() {
+ return this.overAllocatedHistogram_;
+ }
+
+ get bucketSize() {
+ return this.bucketSize_;
+ }
+
+ get overallPercent() {
+ return this.overallPercent_;
+ }
+
+ set overallPercent(value) {
+ this.overallPercent_ = value;
+ }
+
+ get overAllocatedPercent() {
+ return this.overAllocatedPercent_;
+ }
+
+ set overAllocatedPercent(value) {
+ this.overAllocatedPercent_ = value;
+ }
+
+ setFromObject(obj) {
+ this.count_ = obj.count;
+ // Calculate memory in KB.
+ this.overall_ = obj.overall / 1024;
+ this.overAllocated_ = obj.over_allocated / 1024;
+ this.histogram_ = obj.histogram;
+ this.overAllocatedHistogram_ = obj.over_allocated_histogram;
+ }
+
+ diff(other) {
+ const entry = new Entry(this.title_, other.count_ - this.count,
+ other.overall_ - this.overall,
+ other.overAllocated_ - this.overAllocated, [], []);
+ entry.overallPercent = computePercentage(entry.overall, this.overall);
+ entry.overAllocatedPercent = computePercentage(entry.overAllocated,
+ this.overAllocated);
+ return new DiffEntry(this, entry);
+ }
+ }
+
+ class GroupedEntry extends Entry {
+ constructor(title, count, overall, overAllocated, histogram,
+ overAllocatedHistogram) {
+ super(title, count, overall, overAllocated, histogram,
+ overAllocatedHistogram);
+ this.histogram_.fill(0);
+ this.overAllocatedHistogram_.fill(0);
+ this.entries_ = new Map();
+ }
+
+ get title() {
+ return this.title_;
+ }
+
+ set title(value) {
+ this.title_ = value;
+ }
+
+ get subRows() {
+ return Array.from(this.entries_.values());
+ }
+
+ getEntryFromTitle(title) {
+ return this.entries_.get(title);
+ }
+
+ add(entry) {
+ this.count_ += entry.count;
+ this.overall_ += entry.overall;
+ this.overAllocated_ += entry.overAllocated;
+ if (this.bucketSize_ === entry.bucketSize) {
+ for (let i = 0; i < this.bucketSize_; ++i) {
+ this.histogram_[i] += entry.histogram[i];
+ this.overAllocatedHistogram_[i] += entry.overAllocatedHistogram[i];
+ }
+ }
+ this.entries_.set(entry.title, entry);
+ }
+
+ accumulateUnknown(title) {
+ let unknownCount = this.count_;
+ let unknownOverall = this.overall_;
+ let unknownOverAllocated = this.overAllocated_;
+ const unknownHistogram = tr.b.deepCopy(this.histogram_);
+ const unknownOverAllocatedHistogram =
+ tr.b.deepCopy(this.overAllocatedHistogram_);
+ for (const entry of this.entries_.values()) {
+ unknownCount -= entry.count;
+ unknownOverall -= entry.overall;
+ unknownOverAllocated -= entry.overAllocated;
+ for (let i = 0; i < this.bucketSize_; ++i) {
+ unknownHistogram[i] -= entry.histogram[i];
+ unknownOverAllocatedHistogram[i] -= entry.overAllocatedHistogram[i];
+ }
+ }
+ unknownOverAllocated =
+ unknownOverAllocated < 0 ? 0 : unknownOverAllocated;
+ this.entries_.set(title, new Entry(title, unknownCount, unknownOverall,
+ unknownOverAllocated, unknownHistogram,
+ unknownOverAllocatedHistogram));
+ }
+
+ calculatePercentage() {
+ for (const entry of this.entries_.values()) {
+ entry.overallPercent = computePercentage(entry.overall, this.overall_);
+ entry.overAllocatedPercent =
+ computePercentage(entry.overAllocated, this.overAllocated_);
+
+ if (entry instanceof GroupedEntry) entry.calculatePercentage();
+ }
+ }
+
+ diff(other) {
+ let newTitle = '';
+ if (this.title_.startsWith('Isolate')) {
+ newTitle = 'Total';
+ } else {
+ newTitle = this.title_;
+ }
+ const result = new GroupedEntry(newTitle, 0, 0, 0, [], []);
+ for (const entry of this.entries_) {
+ const otherEntry = other.getEntryFromTitle(entry[0]);
+ if (otherEntry === undefined) continue;
+ result.add(entry[1].diff(otherEntry));
+ }
+ result.overallPercent = computePercentage(result.overall, this.overall);
+ result.overAllocatedPercent = computePercentage(result.overAllocated,
+ this.overAllocated);
+ return new DiffEntry(this, result);
+ }
+ }
+
+ function createSelector(targetEl, defaultValue, items, callback) {
+ const selectorEl = document.createElement('select');
+ selectorEl.addEventListener('change', callback.bind(targetEl));
+ const defaultOptionEl = document.createElement('option');
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ const optionEl = document.createElement('option');
+ Polymer.dom(optionEl).textContent = item.label;
+ optionEl.targetPropertyValue = item.value;
+ optionEl.item = item;
+ Polymer.dom(selectorEl).appendChild(optionEl);
+ }
+ selectorEl.__defineGetter__('selectedValue', function(v) {
+ if (selectorEl.children[selectorEl.selectedIndex] === undefined) {
+ return undefined;
+ }
+ return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue;
+ });
+ selectorEl.__defineGetter__('selectedItem', function(v) {
+ if (selectorEl.children[selectorEl.selectedIndex] === undefined) {
+ return undefined;
+ }
+ return selectorEl.children[selectorEl.selectedIndex].item;
+ });
+ selectorEl.__defineSetter__('selectedValue', function(v) {
+ for (let i = 0; i < selectorEl.children.length; i++) {
+ const value = selectorEl.children[i].targetPropertyValue;
+ if (value === v) {
+ const changed = selectorEl.selectedIndex !== i;
+ if (changed) {
+ selectorEl.selectedIndex = i;
+ callback();
+ }
+ return;
+ }
+ }
+ throw new Error('Not a valid value');
+ });
+ selectorEl.selectedIndex = -1;
+
+ return selectorEl;
+ }
+
+ function plusMinus(value, toFixed = 3) {
+ return (value > 0 ? '+' : '') + value.toFixed(toFixed);
+ }
+
+ function addArrow(value) {
+ if (value === 0) return value;
+ if (value === Number.NEGATIVE_INFINITY) return '\u2193\u221E';
+ if (value === Number.POSITIVE_INFINITY) return '\u2191\u221E';
+ return (value > 0 ? '\u2191' : '\u2193') + Math.abs(value.toFixed(3));
+ }
+
+ Polymer({
+ is: 'tr-ui-e-v8-gc-objects-stats-table',
+
+ ready() {
+ this.$.diffOption.style.display = 'none';
+ this.isolateEntries_ = [];
+ this.selector1_ = undefined;
+ this.selector2_ = undefined;
+ },
+
+ constructDiffTable_(table) {
+ this.$.diffTable.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.diffTable.tableColumns = [
+ {
+ title: 'Component',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.title;
+ return typeEl;
+ },
+ showExpandButtons: true
+ },
+ {
+ title: 'Overall Memory(KB)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = row.origin.overall.toFixed(3);
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.origin.overall - b.origin.overall;
+ }
+ },
+ {
+ title: 'diff(KB)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = plusMinus(row.overall);
+ if (row.overall > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.overall < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.overall - b.overall;
+ }
+ },
+ {
+ title: 'diff(%)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = addArrow(row.overallPercent);
+ if (row.overall > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.overall < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.overall - b.overall;
+ }
+ },
+ {
+ title: 'Over Allocated Memory(KB)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = row.origin.overAllocated.toFixed(3);
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.origin.overAllocated - b.origin.overAllocated;
+ }
+ },
+ {
+ title: 'diff(KB)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = plusMinus(row.overAllocated);
+ if (row.overAllocated > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.overAllocated < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.overAllocated - b.overAllocated;
+ }
+ },
+ {
+ title: 'diff(%)',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = addArrow(row.overAllocatedPercent);
+ if (row.overAllocated > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.overAllocated < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.overAllocated - b.overAllocated;
+ }
+ },
+ {
+ title: 'Count',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = row.origin.count;
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.origin.count - b.origin.count;
+ }
+ },
+ {
+ title: 'diff',
+ value(row) {
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = plusMinus(row.count, 0);
+ if (row.count > 0) {
+ spanEl.style.color = DIFF_COLOR.RED;
+ } else if (row.count < 0) {
+ spanEl.style.color = DIFF_COLOR.GREEN;
+ }
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.count - b.count;
+ }
+ },
+ ];
+ },
+
+ buildOptions_() {
+ const items = [];
+ for (const isolateEntry of this.isolateEntries_) {
+ items.push({
+ label: isolateEntry.title,
+ value: isolateEntry
+ });
+ }
+ this.$.diffOption.style.display = 'inline-block';
+ this.selector1_ = createSelector(
+ this, '', items, this.diffOptionChanged_);
+ Polymer.dom(this.$.diffOption).appendChild(this.selector1_);
+ const spanEl = tr.ui.b.createSpan();
+ spanEl.innerText = ' VS ';
+ Polymer.dom(this.$.diffOption).appendChild(spanEl);
+ this.selector2_ = createSelector(
+ this, '', items, this.diffOptionChanged_);
+ Polymer.dom(this.$.diffOption).appendChild(this.selector2_);
+ },
+
+ diffOptionChanged_() {
+ const isolateEntry1 = this.selector1_.selectedValue;
+ const isolateEntry2 = this.selector2_.selectedValue;
+ if (isolateEntry1 === undefined || isolateEntry2 === undefined) {
+ return;
+ }
+ if (isolateEntry1 === isolateEntry2) {
+ this.$.diffTable.tableRows = [];
+ this.$.diffTable.rebuild();
+ return;
+ }
+ this.$.diffTable.tableRows = [isolateEntry1.diff(isolateEntry2)];
+ this.$.diffTable.rebuild();
+ },
+
+ constructTable_() {
+ this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ this.$.table.tableColumns = [
+ {
+ title: 'Component',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.title;
+ return typeEl;
+ },
+ showExpandButtons: true
+ },
+ {
+ title: 'Overall Memory (KB)',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.overall.toFixed(3);
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.overall - b.overall;
+ }
+ },
+ {
+ title: 'Over Allocated Memory (KB)',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.overAllocated.toFixed(3);
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.overAllocated - b.overAllocated;
+ }
+ },
+ {
+ title: 'Overall Count',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.count;
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.count - b.count;
+ }
+ },
+ {
+ title: 'Overall Memory Percent',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.overallPercent.toFixed(3) + '%';
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.overall - b.overall;
+ }
+ },
+ {
+ title: 'Overall Allocated Memory Percent',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.overAllocatedPercent.toFixed(3) + '%';
+ return typeEl;
+ },
+ cmp(a, b) {
+ return a.overAllocated - b.overAllocated;
+ }
+ }
+ ];
+
+ this.$.table.sortColumnIndex = 1;
+ this.$.table.sortDescending = true;
+ },
+
+ buildSubEntry_(objects, groupEntry, keyToName) {
+ const typeGroup = INSTANCE_TYPE_GROUPS[groupEntry.title];
+ for (const instanceType of typeGroup) {
+ const e = objects[instanceType];
+ if (e === undefined) continue;
+ delete objects[instanceType];
+ let title = instanceType;
+ if (keyToName !== undefined) title = keyToName(title);
+ // Represent memery in KB unit.
+ groupEntry.add(new Entry(title, e.count, e.overall / 1024,
+ e.over_allocated / 1024, e.histogram,
+ e.over_allocated_histogram));
+ }
+ },
+
+ buildUnGroupedEntries_(objects, objectEntry, bucketSize) {
+ for (const title of Object.getOwnPropertyNames(objects)) {
+ const obj = objects[title];
+ const groupedEntry = new GroupedEntry(title, 0, 0, 0,
+ new Array(bucketSize),
+ new Array(bucketSize));
+ groupedEntry.setFromObject(obj);
+ objectEntry.add(groupedEntry);
+ }
+ },
+
+ createGroupEntries_(groupEntries, objects, bucketSize) {
+ for (const groupName of Object.getOwnPropertyNames(
+ INSTANCE_TYPE_GROUPS)) {
+ const groupEntry = new GroupedEntry(groupName, 0, 0, 0,
+ new Array(bucketSize),
+ new Array(bucketSize));
+ if (INSTANCE_TYPE_GROUPS[groupName].realEntry !== undefined) {
+ groupEntry.savedRealEntry =
+ objects[INSTANCE_TYPE_GROUPS[groupName].realEntry];
+ delete objects[INSTANCE_TYPE_GROUPS[groupName].realEntry];
+ }
+ groupEntries[groupName] = groupEntry;
+ }
+ },
+
+ buildGroupEntries_(groupEntries, objectEntry) {
+ for (const groupName of Object.getOwnPropertyNames(groupEntries)) {
+ const groupEntry = groupEntries[groupName];
+ if (groupEntry.savedRealEntry !== undefined) {
+ groupEntry.setFromObject(groupEntry.savedRealEntry);
+ groupEntry.accumulateUnknown('UNKNOWN');
+ delete groupEntry.savedRealEntry;
+ }
+ objectEntry.add(groupEntry);
+ }
+ },
+
+ buildSubEntriesForGroups_(groupEntries, objects) {
+ for (const instanceType of Object.getOwnPropertyNames(objects)) {
+ if (IGNORED_ENTRIES.match(instanceType)) {
+ delete objects[instanceType];
+ continue;
+ }
+ const e = objects[instanceType];
+ for (const name of Object.getOwnPropertyNames(INSTANCE_TYPE_GROUPS)) {
+ const group = INSTANCE_TYPE_GROUPS[name];
+ if (group.match(instanceType)) {
+ groupEntries[name].add(new Entry(
+ group.keyToName(instanceType), e.count, e.overall / 1024,
+ e.over_allocated / 1024, e.histogram,
+ e.over_allocated_histogram));
+ delete objects[instanceType];
+ }
+ }
+ }
+ },
+
+ build_(objects, objectEntry, bucketSize) {
+ delete objects.END;
+ const groupEntries = {};
+ this.createGroupEntries_(groupEntries, objects, bucketSize);
+ this.buildSubEntriesForGroups_(groupEntries, objects);
+ this.buildGroupEntries_(groupEntries, objectEntry);
+ this.buildUnGroupedEntries_(objects, objectEntry, bucketSize);
+ },
+
+ set selection(slices) {
+ slices.sortEvents(function(a, b) {
+ return b.start - a.start;
+ });
+ const previous = undefined;
+ for (const slice of slices) {
+ if (!slice instanceof tr.e.v8.V8GCStatsThreadSlice) continue;
+ const liveObjects = slice.liveObjects;
+ const deadObjects = slice.deadObjects;
+ const isolate = liveObjects.isolate;
+
+ const isolateEntry =
+ new GroupedEntry(
+ 'Isolate_' + isolate + ' at ' + slice.start.toFixed(3) + ' ms',
+ 0, 0, 0, [], []);
+ const liveEntry = new GroupedEntry('live objects', 0, 0, 0, [], []);
+ const deadEntry = new GroupedEntry('dead objects', 0, 0, 0, [], []);
+
+ const liveBucketSize = liveObjects.bucket_sizes.length;
+ const deadBucketSize = deadObjects.bucket_sizes.length;
+
+ this.build_(tr.b.deepCopy(liveObjects.type_data), liveEntry,
+ liveBucketSize);
+ isolateEntry.add(liveEntry);
+
+ this.build_(tr.b.deepCopy(deadObjects.type_data), deadEntry,
+ deadBucketSize);
+ isolateEntry.add(deadEntry);
+
+ isolateEntry.calculatePercentage();
+ this.isolateEntries_.push(isolateEntry);
+ }
+ this.updateTable_();
+
+ if (slices.length > 1) {
+ this.buildOptions_();
+ this.constructDiffTable_();
+ }
+ },
+
+ updateTable_() {
+ this.constructTable_();
+ this.$.table.tableRows = this.isolateEntries_;
+ this.$.table.rebuild();
+ },
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html
new file mode 100644
index 00000000000..42d904d94c8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html">
+<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.s1 = m.t2.sliceGroup.pushSlice(
+ newSliceEx({
+ title: 'V8.GC_Objects_Stats',
+ start: 1,
+ end: 1,
+ type: tr.e.v8.V8GCStatsThreadSlice,
+ cat: 'disabled-by-default-v8.gc_stats',
+ args: {
+ // eslint-disable-next-line
+ live:'{"isolate":"0x00000000001","id":1,"time":111,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"JS_OBJECT_TYPE":{"type":4,"overall":5,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"JS_TYPED_ARRAY_TYPE":{"type":5,"overall":5,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"CODE_TYPE":{"type":4,"overall":6,"count":6,"over_allocated":0,"histogram":[6,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*CODE_BYTECODE_HANDLER":{"type":7,"overall":5,"count":6,"over_allocated":0,"histogram":[6,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*CODE_AGE_Quadragenarian":{"type":8,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}',
+ // eslint-disable-next-line
+ dead:'{"isolate":"0x00000000001","id":2,"time":112,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":3,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}'
+ }
+ })
+ );
+ m.s2 = m.t2.sliceGroup.pushSlice(
+ newSliceEx({
+ title: 'V8.GC_Objects_Stats',
+ start: 2,
+ end: 2,
+ type: tr.e.v8.V8GCStatsThreadSlice,
+ cat: 'disabled-by-default-v8.gc_stats',
+ args: {
+ // eslint-disable-next-line
+ live:'{"isolate":"0x00000000001","id":1,"time":113,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":3,"count":4,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":6,"count":7,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}',
+ // eslint-disable-next-line
+ dead:'{"isolate":"0x00000000001","id":2,"time":114,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":4,"count":4,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}'
+ }
+ })
+ );
+ m.s3 = m.t2.sliceGroup.pushSlice(
+ newSliceEx({
+ title: 'V8.GC_Objects_Stats',
+ start: 3,
+ end: 3,
+ type: tr.e.v8.V8GCStatsThreadSlice,
+ cat: 'disabled-by-default-v8.gc_stats',
+ args: {
+ // eslint-disable-next-line
+ live:'{"isolate":"0x00000000001","id":1,"time":115,"bucket_sizes":[32,64,128,256],"type_data":{"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}, "TYPE_DONT_HAVE_GROUP1":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}, "TYPE_DONT_HAVE_GROUP2":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[0,1,0,0],"over_allocated_histogram":[0,0,0,0]}}}',
+ // eslint-disable-next-line
+ dead:'{"isolate":"0x00000000001","id":2,"time":116,"bucket_sizes":[32,64,128,256],"type_data":{"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}'
+ }
+ })
+ );
+ });
+ return m;
+ }
+
+ test('GCObjectTableSingleSelection', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table');
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(m.s1);
+ viewEl.selection = eventSet;
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 1);
+ const row = rows[0];
+ assert.strictEqual(row.overall, 0.0263671875);
+ assert.strictEqual(row.count, 21);
+ assert.strictEqual(row.overAllocated, 0);
+ const subRows = row.subRows;
+ const live = subRows[0];
+ assert.strictEqual(live.overall, 0.0224609375);
+ assert.strictEqual(live.count, 17);
+ assert.strictEqual(live.overAllocated, 0);
+ const dead = subRows[1];
+ assert.strictEqual(dead.overall, 0.00390625);
+ assert.strictEqual(dead.count, 4);
+ assert.strictEqual(dead.overAllocated, 0);
+ });
+
+ test('GCObjectTableMultiSelection', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table');
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(m.s1);
+ eventSet.push(m.s2);
+ viewEl.selection = eventSet;
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 2);
+
+ let row = rows[0];
+ assert.strictEqual(row.overall, 0.0146484375);
+ assert.strictEqual(row.count, 17);
+ assert.strictEqual(row.overAllocated, 0);
+ let subRows = row.subRows;
+ let live = subRows[0];
+ assert.strictEqual(live.overall, 0.0087890625);
+ assert.strictEqual(live.count, 11);
+ assert.strictEqual(live.overAllocated, 0);
+ let dead = subRows[1];
+ assert.strictEqual(dead.overall, 0.005859375);
+ assert.strictEqual(dead.count, 6);
+ assert.strictEqual(dead.overAllocated, 0);
+
+ row = rows[1];
+ assert.strictEqual(row.overall, 0.0263671875);
+ assert.strictEqual(row.count, 21);
+ assert.strictEqual(row.overAllocated, 0);
+ subRows = row.subRows;
+ live = subRows[0];
+ assert.strictEqual(live.overall, 0.0224609375);
+ assert.strictEqual(live.count, 17);
+ assert.strictEqual(live.overAllocated, 0);
+ dead = subRows[1];
+ assert.strictEqual(dead.overall, 0.00390625);
+ assert.strictEqual(dead.count, 4);
+ assert.strictEqual(dead.overAllocated, 0);
+ });
+
+ test('GCObjectTableDiff', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table');
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(m.s1);
+ eventSet.push(m.s2);
+ viewEl.selection = eventSet;
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 2);
+ const diffEntry = rows[0].diff(rows[1]);
+
+ assert.strictEqual(diffEntry.origin.overall.toFixed(3), '0.015');
+ assert.strictEqual(diffEntry.origin.overAllocated, 0);
+ assert.strictEqual(diffEntry.overall.toFixed(3), '-0.004');
+ assert.strictEqual(diffEntry.overAllocated, 0);
+ assert.strictEqual(diffEntry.overallPercent.toFixed(3), '-26.667');
+ assert.strictEqual(diffEntry.overAllocatedPercent, 0);
+ });
+
+ test('GCObjectTableGroupEntryWithoutGroupDefined', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table');
+ const eventSet = new tr.model.EventSet();
+ eventSet.push(m.s3);
+ viewEl.selection = eventSet;
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 1);
+ const row = rows[0];
+ assert.strictEqual(row.overall, 0.013671875);
+ assert.strictEqual(row.count, 18);
+ assert.strictEqual(row.overAllocated, 0);
+ const subRows = row.subRows;
+
+ const live = subRows[0];
+ assert.strictEqual(live.overall, 0.0087890625);
+ assert.strictEqual(live.count, 12);
+ assert.strictEqual(live.overAllocated, 0);
+
+ // ungrouped entry should be top level entry.
+ const unGrouped1 = live.getEntryFromTitle('TYPE_DONT_HAVE_GROUP1');
+ assert.isDefined(unGrouped1);
+ assert.strictEqual(unGrouped1.overall, 0.001953125);
+ assert.strictEqual(unGrouped1.count, 3);
+ assert.strictEqual(unGrouped1.overAllocated, 0);
+
+ const unGrouped2 = live.getEntryFromTitle('TYPE_DONT_HAVE_GROUP2');
+ assert.isDefined(unGrouped2);
+ assert.strictEqual(unGrouped2.overall, 0.001953125);
+ assert.strictEqual(unGrouped2.count, 3);
+ assert.strictEqual(unGrouped2.overAllocated, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html
new file mode 100644
index 00000000000..e19a367cfca
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/v8/ic_stats_entry.html">
+<link rel="import" href="/tracing/extras/v8/v8_ic_stats_thread_slice.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-e-v8-ic-stats-table'>
+ <template>
+ <style>
+ tr-ui-b-table {
+ flex: 0 0 auto;
+ align-self: stretch;
+ margin-top: 1em;
+ font-size: 12px;
+ }
+ #total {
+ margin-top: 1em;
+ margin-left: 0.8em;
+ }
+ #groupOption {
+ display: inline-block;
+ margin-top: 1em;
+ margin-left: 0.8em;
+ }
+ </style>
+ <div style="padding-right: 200px">
+ <div style="float:right; border-style: solid; border-width: 1px; padding:20px">
+ 0 uninitialized<br>
+ . premonomorphic<br>
+ 1 monomorphic<br>
+ ^ recompute handler<br>
+ P polymorphic<br>
+ N megamorphic<br>
+ G generic
+ </div>
+ </div>
+ <div id="total">
+ </div>
+ <div id="groupOption">
+ Group Key
+ </div>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.v8', function() {
+ const PROPERTIES = tr.e.v8.IC_STATS_PROPERTIES.map(
+ x => {return {label: x, value: x};});
+ const ICStatsEntry = tr.e.v8.ICStatsEntry;
+ const ICStatsEntryGroup = tr.e.v8.ICStatsEntryGroup;
+ const ICStatsCollection = tr.e.v8.ICStatsCollection;
+
+ Polymer({
+ is: 'tr-ui-e-v8-ic-stats-table',
+
+ ready() {
+ this.icStatsCollection_ = new ICStatsCollection();
+ this.groupKey_ = PROPERTIES[0].value;
+ this.selector_ = tr.ui.b.createSelector(this, 'groupKey',
+ 'v8ICStatsGroupKey',
+ this.groupKey_, PROPERTIES);
+ Polymer.dom(this.$.groupOption).appendChild(this.selector_);
+ },
+
+ get groupKey() {
+ return this.groupKey_;
+ },
+
+ set groupKey(key) {
+ this.groupKey_ = key;
+ if (this.icStatsCollection_.length === 0) return;
+ this.updateTable_(this.groupKey_);
+ },
+
+ constructTable_(table, groupKey) {
+ table.tableColumns = [
+ {
+ title: '',
+ value: row => {
+ let expanded = false;
+ const buttonEl = tr.ui.b.createButton('details', function() {
+ const previousSibling = Polymer.dom(this).parentNode.parentNode;
+ const parentNode = previousSibling.parentNode;
+ if (expanded) {
+ const trEls = parentNode.getElementsByClassName('subTable');
+ Array.from(trEls).map(x => x.parentNode.removeChild(x));
+ expanded = false;
+ return;
+ }
+ expanded = true;
+ const subGroups = row.createSubGroup();
+ const tr = document.createElement('tr');
+ tr.classList.add('subTable');
+ tr.appendChild(document.createElement('td'));
+ const td = document.createElement('td');
+ td.colSpan = 3;
+ for (const subGroup of subGroups) {
+ const property = subGroup[0];
+ const all = Array.from(subGroup[1].values());
+ const group = all.slice(0, 20);
+ const divEl = document.createElement('div');
+ const spanEl = document.createElement('span');
+ const subTableEl = document.createElement('tr-ui-b-table');
+
+ spanEl.innerText = `Top 20 out of ${all.length}`;
+ spanEl.style.fontWeight = 'bold';
+ spanEl.style.fontSize = '14px';
+ divEl.appendChild(spanEl);
+
+ this.constructTable_(subTableEl, property);
+ subTableEl.tableRows = group;
+ subTableEl.rebuild();
+ divEl.appendChild(subTableEl);
+ td.appendChild(divEl);
+ }
+ tr.appendChild(td);
+ parentNode.insertBefore(tr, previousSibling.nextSibling);
+ });
+ return buttonEl;
+ }
+ },
+ {
+ title: 'Percentage',
+ value(row) {
+ const spanEl = document.createElement('span');
+ spanEl.innerText = (row.percentage * 100).toFixed(3) + '%';
+ return spanEl;
+ },
+ cmp: (a, b) => a.percentage - b.percentage
+ },
+ {
+ title: 'Count',
+ value(row) {
+ const spanEl = document.createElement('span');
+ spanEl.innerText = row.length;
+ return spanEl;
+ },
+ cmp: (a, b) => a.length - b.length
+ },
+ {
+ title: groupKey,
+ value(row) {
+ const spanEl = document.createElement('span');
+ spanEl.innerText = row.key ? row.key : '';
+ return spanEl;
+ }
+ }
+ ];
+
+ table.sortColumnIndex = 1;
+ table.sortDescending = true;
+ },
+
+ updateTable_(groupKey) {
+ this.constructTable_(this.$.table, groupKey);
+ this.$.table.tableRows = this.icStatsCollection_.groupBy(groupKey);
+ this.$.table.rebuild();
+ },
+
+ set selection(slices) {
+ for (const slice of slices) {
+ for (const icStatsObj of slice.icStats) {
+ const entry = new ICStatsEntry(icStatsObj);
+ this.icStatsCollection_.add(entry);
+ }
+ }
+ this.$.total.innerText = 'Total items: ' + this.icStatsCollection_.length;
+ this.updateTable_(this.selector_.selectedValue);
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html
new file mode 100644
index 00000000000..eded2c44a66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html">
+
+<dom-module id='tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view'>
+ <template>
+ <style>
+ </style>
+ <tr-ui-e-v8-gc-objects-stats-table id="gcObjectsStats">
+ </tr-ui-e-v8-gc-objects-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.gcObjectsStats.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view',
+ tr.e.v8.V8GCStatsThreadSlice,
+ {
+ multi: true,
+ title: 'V8 GC Stats slices'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html
new file mode 100644
index 00000000000..36b14cb7be0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/ic_stats_table.html">
+
+<dom-module id='tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view'>
+ <template>
+ <tr-ui-e-v8-ic-stats-table id="table">
+ </tr-ui-e-v8-ic-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.table.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view',
+ tr.e.v8.V8ICStatsThreadSlice,
+ {
+ multi: true,
+ title: 'V8 IC stats slices'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html
new file mode 100644
index 00000000000..48c1f05b274
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html">
+
+<dom-module id='tr-ui-e-multi-v8-thread-slice-sub-view'>
+ <template>
+ <tr-ui-a-multi-thread-slice-sub-view id="content"></tr-ui-a-multi-thread-slice-sub-view>
+ <tr-ui-e-v8-runtime-call-stats-table id="runtimeCallStats"></tr-ui-e-v8-runtime-call-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-e-multi-v8-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.runtimeCallStats.slices = selection;
+ this.$.content.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-multi-v8-thread-slice-sub-view',
+ tr.e.v8.V8ThreadSlice,
+ {
+ multi: true,
+ title: 'V8 slices'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html
new file mode 100644
index 00000000000..d0d62bed87d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.s1 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ CompileFullCode: [3, 345],
+ LoadIC_Miss: [5, 567],
+ ParseLazy: [8, 890]
+ }}}));
+ m.s2 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 11,
+ end: 15,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ HandleApiCall: [1, 123],
+ OptimizeCode: [7, 789]
+ }}}));
+ });
+ return m;
+ }
+
+ test('selectMultiV8ThreadSlices', function() {
+ const m = createModel();
+
+ const viewEl =
+ document.createElement('tr-ui-e-multi-v8-thread-slice-sub-view');
+ const selection = new tr.model.EventSet();
+ selection.push(m.s1);
+ selection.push(m.s2);
+ viewEl.selection = selection;
+ this.addHTMLOutput(viewEl);
+ const rows = viewEl.$.runtimeCallStats.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 2714,
+ 567,
+ 0,
+ 789,
+ 0,
+ 345,
+ 0,
+ 890,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 123
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html
new file mode 100644
index 00000000000..27bf0f5ef72
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html
@@ -0,0 +1,197 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-e-v8-runtime-call-stats-table'>
+ <template>
+ <style>
+ #table, #blink_rcs_table {
+ flex: 0 0 auto;
+ align-self: stretch;
+ margin-top: 1em;
+ font-size: 12px;
+ }
+
+ #v8_rcs_heading, #blink_rcs_heading {
+ padding-top: 1em;
+ font-size: 18px;
+ }
+ </style>
+ <h1 id="v8_rcs_heading"></h1>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ <h1 id="blink_rcs_heading"></h1>
+ <tr-ui-b-table id="blink_rcs_table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.e.v8', function() {
+ const codeSearchURL_ = 'https://cs.chromium.org/search/?sq=package:chromium&type=cs&q=';
+
+ function removeBlinkPrefix_(name) {
+ if (name.startsWith('Blink_')) name = name.substring(6);
+ return name;
+ }
+
+ function handleCodeSearchForV8_(event) {
+ if (event.target.parentNode === undefined) return;
+ let name = event.target.parentNode.entryName;
+ if (name.startsWith('API_')) name = name.substring(4);
+ const url = codeSearchURL_ + encodeURIComponent(name) + '+file:src/v8/src';
+ window.open(url, '_blank');
+ }
+
+ function handleCodeSearchForBlink_(event) {
+ if (event.target.parentNode === undefined) return;
+ const name = event.target.parentNode.entryName;
+ const url = codeSearchURL_ +
+ encodeURIComponent('RuntimeCallStats::CounterId::k' + name) +
+ '+file:src/third_party/WebKit/|src/out/Debug/';
+ window.open(url, '_blank');
+ }
+
+ function createCodeSearchEl_(handleCodeSearch) {
+ const codeSearchEl = document.createElement('span');
+ codeSearchEl.innerText = '?';
+ codeSearchEl.style.float = 'right';
+ codeSearchEl.style.borderRadius = '5px';
+ codeSearchEl.style.backgroundColor = '#EEE';
+ codeSearchEl.addEventListener('click',
+ handleCodeSearch.bind(this));
+ return codeSearchEl;
+ }
+
+ const timeColumn_ = {
+ title: 'Time',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = (row.time / 1000.0).toFixed(3) + ' ms';
+ return typeEl;
+ },
+ width: '100px',
+ cmp(a, b) {
+ return a.time - b.time;
+ }
+ };
+
+ const countColumn_ = {
+ title: 'Count',
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = row.count;
+ return typeEl;
+ },
+ width: '100px',
+ cmp(a, b) {
+ return a.count - b.count;
+ }
+ };
+
+ function percentColumn_(title, totalTime) {
+ return {
+ title,
+ value(row) {
+ const typeEl = document.createElement('span');
+ typeEl.innerText = (row.time / totalTime * 100).toFixed(3) + '%';
+ return typeEl;
+ },
+ width: '100px',
+ cmp(a, b) {
+ return a.time - b.time;
+ }
+ };
+ }
+
+ function nameColumn_(handleCodeSearch, modifyName) {
+ return {
+ title: 'Name',
+ value(row) {
+ const typeEl = document.createElement('span');
+ let name = row.name;
+ if (modifyName) name = modifyName(name);
+ typeEl.innerText = name;
+ if (!(row instanceof tr.e.v8.RuntimeStatsGroup)) {
+ typeEl.title = 'click ? for code search';
+ typeEl.entryName = name;
+ const codeSearchEl = createCodeSearchEl_(handleCodeSearch);
+ typeEl.appendChild(codeSearchEl);
+ }
+ return typeEl;
+ },
+ width: '200px',
+ showExpandButtons: true
+ };
+ }
+
+ function initializeCommonOptions_(table) {
+ table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+ table.sortColumnIndex = 1;
+ table.sortDescending = true;
+ table.subRowsPropertyName = 'values';
+ }
+
+ Polymer({
+ is: 'tr-ui-e-v8-runtime-call-stats-table',
+
+ ready() {
+ this.table_ = this.$.table;
+ this.blink_rcs_table_ = this.$.blink_rcs_table;
+ this.totalTime_ = 0;
+ },
+
+ constructV8RCSTable_(totalTime) {
+ this.table_.tableColumns = [
+ nameColumn_(handleCodeSearchForV8_),
+ timeColumn_,
+ countColumn_,
+ percentColumn_('Percent', totalTime)
+ ];
+
+ initializeCommonOptions_(this.table_);
+ },
+
+ constructBlinkRCSTable_(blinkCppTotalTime) {
+ this.blink_rcs_table_.tableColumns = [
+ nameColumn_(handleCodeSearchForBlink_, removeBlinkPrefix_),
+ timeColumn_,
+ countColumn_,
+ percentColumn_('Percent (of \'Blink C++\' + \'API\')',
+ blinkCppTotalTime)
+ ];
+
+ initializeCommonOptions_(this.blink_rcs_table_);
+ },
+
+ set slices(slices) {
+ const runtimeGroupCollection = new tr.e.v8.RuntimeStatsGroupCollection();
+ runtimeGroupCollection.addSlices(slices);
+ if (runtimeGroupCollection.totalTime > 0) {
+ this.$.v8_rcs_heading.textContent = 'V8 Runtime Call Stats';
+ this.constructV8RCSTable_(runtimeGroupCollection.totalTime);
+ this.table_.tableRows = runtimeGroupCollection.runtimeGroups;
+ this.table_.rebuild();
+ }
+
+ const blinkRCSGroupCollection =
+ runtimeGroupCollection.blinkRCSGroupCollection;
+ if (runtimeGroupCollection.blinkCppTotalTime > 0 &&
+ blinkRCSGroupCollection.totalTime > 0) {
+ this.$.blink_rcs_heading.textContent = 'Blink Runtime Call Stats';
+ this.constructBlinkRCSTable_(runtimeGroupCollection.blinkCppTotalTime);
+ this.blink_rcs_table_.tableRows = blinkRCSGroupCollection.runtimeGroups;
+ this.blink_rcs_table_.rebuild();
+ }
+ }
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html
new file mode 100644
index 00000000000..06698d03c4a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+ const apiObjectGet = [1, 123];
+ const functionCallback = [2, 234];
+ const compileFullCode = [3, 345];
+ const allocateInTargetSpace = [4, 456];
+ const loadIcMiss = [5, 567];
+ const jsExecution = [6, 678];
+ const optimizeCode = [7, 789];
+ const parseLazy = [8, 890];
+ const handleApiCall = [9, 901];
+ const compileBackground = [1, 101];
+ const parseBackground = [2, 202];
+ const optimizeCodeBackground = [3, 303];
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.s1 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ JS_Execution: jsExecution,
+ HandleApiCall: handleApiCall,
+ CompileFullCode: compileFullCode,
+ LoadIC_Miss: loadIcMiss,
+ ParseLazy: parseLazy,
+ RecompileConcurrent: optimizeCode,
+ OptimizeCode: optimizeCode,
+ FunctionCallback: functionCallback,
+ AllocateInTargetSpace: allocateInTargetSpace,
+ API_Object_Get: apiObjectGet,
+ CompileBackgroundIgnition: compileBackground,
+ ParseBackgroundFunctionLiteral: parseBackground,
+ RecompileConcurrent: optimizeCodeBackground
+ }}}));
+ m.s2 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 11,
+ end: 15,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ JS_Execution: jsExecution,
+ HandleApiCall: handleApiCall,
+ CompileFullCode: compileFullCode,
+ LoadIC_Miss: loadIcMiss,
+ ParseLazy: parseLazy,
+ OptimizeCode: optimizeCode,
+ FunctionCallback: functionCallback,
+ AllocateInTargetSpace: allocateInTargetSpace,
+ API_Object_Get: apiObjectGet
+ }}}));
+ m.s3 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 11,
+ end: 15,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ LoadIC_LoadCallback: [1, 111],
+ StoreIC_StoreCallback: [2, 222],
+ }}}));
+ });
+ return m;
+ }
+
+ test('SingleSliceSelection', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement(
+ 'tr-ui-e-v8-runtime-call-stats-table');
+ viewEl.slices = [m.s1];
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 5589,
+ loadIcMiss[1],
+ optimizeCodeBackground[1],
+ optimizeCode[1],
+ compileBackground[1],
+ compileFullCode[1],
+ parseBackground[1],
+ parseLazy[1],
+ functionCallback[1],
+ apiObjectGet[1],
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ allocateInTargetSpace[1],
+ jsExecution[1],
+ handleApiCall[1]
+ ]);
+ });
+
+ test('MultiSliceSelection', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement(
+ 'tr-ui-e-v8-runtime-call-stats-table');
+ viewEl.slices = [m.s1, m.s2];
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 10572,
+ loadIcMiss[1] * 2,
+ optimizeCodeBackground[1],
+ optimizeCode[1] * 2,
+ compileBackground[1],
+ compileFullCode[1] * 2,
+ parseBackground[1],
+ parseLazy[1] * 2,
+ functionCallback[1] * 2,
+ apiObjectGet[1] * 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ allocateInTargetSpace[1] * 2,
+ jsExecution[1] * 2,
+ handleApiCall[1] * 2
+ ]);
+
+ assert.deepEqual(rows.map(r => r.entries_.size), [
+ 0,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 1,
+ 1
+ ]);
+ });
+
+ test('groupCorrectly', function() {
+ const m = createModel();
+
+ const viewEl = document.createElement(
+ 'tr-ui-e-v8-runtime-call-stats-table');
+ viewEl.slices = [m.s3];
+ this.addHTMLOutput(viewEl);
+ tr.b.forceAllPendingTasksToRunForTest();
+ const rows = viewEl.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 333,
+ 333,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]);
+
+ assert.deepEqual(rows.map(r => r.entries_.size), [
+ 0,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html
new file mode 100644
index 00000000000..6a8d5f15b73
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html">
+
+<dom-module id='tr-ui-e-single-v8-gc-stats-thread-slice-sub-view'>
+ <template>
+ <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view>
+ <tr-ui-e-v8-gc-objects-stats-table id="gcObjectsStats"></tr-ui-e-v8-gc-objects-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-single-v8-gc-stats-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.content.selection = selection;
+ this.$.gcObjectsStats.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-single-v8-gc-stats-thread-slice-sub-view',
+ tr.e.v8.V8GCStatsThreadSlice,
+ {
+ multi: false,
+ title: 'V8 GC stats slice'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html
new file mode 100644
index 00000000000..eeaf407eab1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/ic_stats_table.html">
+
+<dom-module id='tr-ui-e-single-v8-ic-stats-thread-slice-sub-view'>
+ <template>
+ <tr-ui-e-v8-ic-stats-table id="table">
+ </tr-ui-e-v8-ic-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-single-v8-ic-stats-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.table.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-single-v8-ic-stats-thread-slice-sub-view',
+ tr.e.v8.V8ICStatsThreadSlice,
+ {
+ multi: false,
+ title: 'V8 IC stats slice'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html
new file mode 100644
index 00000000000..a9b1189fc76
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html">
+
+<dom-module id='tr-ui-e-single-v8-thread-slice-sub-view'>
+ <template>
+ <tr-ui-a-single-thread-slice-sub-view id="content"></tr-ui-a-single-thread-slice-sub-view>
+ <tr-ui-e-v8-runtime-call-stats-table id="runtimeCallStats"></tr-ui-e-v8-runtime-call-stats-table>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-e-single-v8-thread-slice-sub-view',
+ behaviors: [tr.ui.analysis.AnalysisSubView],
+
+ get selection() {
+ return this.$.content.selection;
+ },
+
+ set selection(selection) {
+ this.$.runtimeCallStats.slices = selection;
+ this.$.content.selection = selection;
+ }
+});
+
+tr.ui.analysis.AnalysisSubView.register(
+ 'tr-ui-e-single-v8-thread-slice-sub-view',
+ tr.e.v8.V8ThreadSlice,
+ {
+ multi: false,
+ title: 'V8 slice'
+ }
+);
+
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html
new file mode 100644
index 00000000000..9e12aa6e044
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModel(function(m) {
+ m.p1 = m.getOrCreateProcess(1);
+ m.t2 = m.p1.getOrCreateThread(2);
+
+ m.s1 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 0,
+ end: 10,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ CompileFullCode: [3, 345],
+ LoadIC_Miss: [5, 567],
+ ParseLazy: [8, 890]
+ }}}));
+ m.s2 = m.t2.sliceGroup.pushSlice(
+ newSliceEx(
+ {title: 'V8.Execute',
+ start: 11,
+ end: 15,
+ type: tr.e.v8.V8ThreadSlice,
+ cat: 'v8',
+ args: {'runtime-call-stats':
+ {
+ HandleApiCall: [1, 123],
+ OptimizeCode: [7, 789]
+ }}}));
+ });
+ return m;
+ }
+
+ test('selectV8ThreadSlice', function() {
+ const m = createModel();
+
+ const viewEl =
+ document.createElement('tr-ui-e-single-v8-thread-slice-sub-view');
+ const selection1 = new tr.model.EventSet();
+ selection1.push(m.s1);
+ viewEl.selection = selection1;
+ this.addHTMLOutput(viewEl);
+ let rows = viewEl.$.runtimeCallStats.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 1802,
+ 567,
+ 0,
+ 0,
+ 0,
+ 345,
+ 0,
+ 890,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]);
+
+ const selection2 = new tr.model.EventSet();
+ selection2.push(m.s2);
+ viewEl.selection = selection2;
+ rows = viewEl.$.runtimeCallStats.$.table.tableRows;
+ assert.lengthOf(rows, 19);
+ assert.deepEqual(rows.map(r => r.time), [
+ 912,
+ 0,
+ 0,
+ 789,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 123
+ ]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html
new file mode 100644
index 00000000000..f78005c2b54
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+
+<link rel="import" href="/tracing/extras/v8_config.html">
+<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html">
+<link rel="import" href="/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html">
+<link rel="import" href="/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html">
+<link rel="import" href="/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html">
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_control.html b/chromium/third_party/catapult/tracing/tracing/ui/find_control.html
new file mode 100644
index 00000000000..daaa0f59777
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/find_control.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/find_controller.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<dom-module id='tr-ui-find-control'>
+ <template>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ display: flex;
+ position: relative;
+ }
+ input {
+ -webkit-user-select: auto;
+ background-color: #f8f8f8;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ width: 170px;
+ }
+ input:focus {
+ background-color: white;
+ }
+ tr-ui-b-toolbar-button {
+ border-left: none;
+ margin: 0;
+ }
+ #hitCount {
+ left: 0;
+ opacity: 0.25;
+ pointer-events: none;
+ position: absolute;
+ text-align: right;
+ top: 2px;
+ width: 167px;
+ z-index: 1;
+ }
+ #spinner {
+ visibility: hidden;
+ width: 8px;
+ height: 8px;
+ left: 154px;
+ pointer-events: none;
+ position: absolute;
+ top: 4px;
+ z-index: 1;
+
+ border: 2px solid transparent;
+ border-bottom: 2px solid rgba(0, 0, 0, 0.5);
+ border-right: 2px solid rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ }
+ @keyframes spin { 100% { transform: rotate(360deg); } }
+ </style>
+
+ <input type='text' id='filter'
+ on-input="filterTextChanged"
+ on-keydown="filterKeyDown"
+ on-blur="filterBlur"
+ on-focus="filterFocus"
+ on-mouseup="filterMouseUp" />
+ <div id="spinner"></div>
+ <tr-ui-b-toolbar-button on-click="findPrevious">
+ &larr;
+ </tr-ui-b-toolbar-button>
+ <tr-ui-b-toolbar-button on-click="findNext">
+ &rarr;
+ </tr-ui-b-toolbar-button>
+ <div id="hitCount">0 of 0</div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-find-control',
+
+ filterKeyDown(e) {
+ if (e.keyCode === 27) {
+ const hkc = tr.b.getHotkeyControllerForElement(this);
+ if (hkc) {
+ hkc.childRequestsBlur(this);
+ } else {
+ this.blur();
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ } else if (e.keyCode === 13) {
+ if (e.shiftKey) {
+ this.findPrevious();
+ } else {
+ this.findNext();
+ }
+ }
+ },
+
+ filterBlur(e) {
+ this.updateHitCountEl();
+ },
+
+ filterFocus(e) {
+ this.$.filter.select();
+ },
+
+ // Prevent that the input text is deselected after focusing the find
+ // control with the mouse.
+ filterMouseUp(e) {
+ e.preventDefault();
+ },
+
+ get controller() {
+ return this.controller_;
+ },
+
+ set controller(c) {
+ this.controller_ = c;
+ this.updateHitCountEl();
+ },
+
+ focus() {
+ this.$.filter.focus();
+ },
+
+ get hasFocus() {
+ return this === document.activeElement;
+ },
+
+ filterTextChanged() {
+ Polymer.dom(this.$.hitCount).textContent = '';
+ this.$.spinner.style.visibility = 'visible';
+ this.$.spinner.style.animation = 'spin 1s linear infinite';
+ this.controller.startFiltering(this.$.filter.value).then(function() {
+ this.$.spinner.style.visibility = 'hidden';
+ this.$.spinner.style.animation = '';
+ this.updateHitCountEl();
+ }.bind(this));
+ },
+
+ findNext() {
+ if (this.controller) {
+ this.controller.findNext();
+ }
+ this.updateHitCountEl();
+ },
+
+ findPrevious() {
+ if (this.controller) {
+ this.controller.findPrevious();
+ }
+ this.updateHitCountEl();
+ },
+
+ updateHitCountEl() {
+ if (!this.controller || this.$.filter.value.length === 0) {
+ Polymer.dom(this.$.hitCount).textContent = '';
+ return;
+ }
+
+ const n = this.controller.filterHits.length;
+ const i = n === 0 ? -1 : this.controller.currentHitIndex;
+ Polymer.dom(this.$.hitCount).textContent = (i + 1) + ' of ' + n;
+ },
+
+ setText(string) {
+ this.$.filter.value = string;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html b/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html
new file mode 100644
index 00000000000..e3af0ec9c6f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/find_control_test.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/find_control.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const ctl = document.createElement('tr-ui-find-control');
+ ctl.controller = {
+ findNext() { },
+ findPrevious() { },
+ reset() {},
+
+ filterHits: ['a', 'b'],
+
+ currentHitIndex: 0
+ };
+
+ this.addHTMLOutput(ctl);
+ });
+
+ test('updateHitCountEl_twoResults', function() {
+ const ctl = document.createElement('tr-ui-find-control');
+ ctl.controller = {
+ findNext() { },
+ findPrevious() { },
+ reset() {},
+
+ filterHits: ['a', 'b'],
+
+ currentHitIndex: 0
+ };
+
+ this.addHTMLOutput(ctl);
+ ctl.$.filter.value = 'test';
+ ctl.updateHitCountEl();
+ assert.strictEqual(ctl.$.hitCount.textContent, '1 of 2');
+ });
+
+ test('updateHitCountEl_emptyFilter', function() {
+ const ctl = document.createElement('tr-ui-find-control');
+ ctl.controller = {
+ findNext() { },
+ findPrevious() { },
+ reset() {},
+
+ filterHits: ['a', 'b'],
+
+ currentHitIndex: 0
+ };
+
+ this.addHTMLOutput(ctl);
+ ctl.$.filter.value = '';
+ ctl.updateHitCountEl();
+ assert.strictEqual(ctl.$.hitCount.textContent, '');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html
new file mode 100644
index 00000000000..926915b19fb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/find_controller.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event_set.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview FindController.
+ */
+tr.exportTo('tr.ui', function() {
+ const Task = tr.b.Task;
+
+ function FindController(brushingStateController) {
+ this.brushingStateController_ = brushingStateController;
+ this.filterHits_ = [];
+ this.currentHitIndex_ = -1;
+ this.activePromise_ = Promise.resolve();
+ this.activeTask_ = undefined;
+ }
+
+ FindController.prototype = {
+ __proto__: Object.prototype,
+
+ get model() {
+ return this.brushingStateController_.model;
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ enqueueOperation_(operation) {
+ let task;
+ if (operation instanceof tr.b.Task) {
+ task = operation;
+ } else {
+ task = new tr.b.Task(operation, this);
+ }
+ if (this.activeTask_) {
+ this.activeTask_ = this.activeTask_.enqueue(task);
+ } else {
+ // We're enqueuing the first task, schedule it.
+ this.activeTask_ = task;
+ this.activePromise_ = Task.RunWhenIdle(this.activeTask_);
+ this.activePromise_.then(function() {
+ this.activePromise_ = undefined;
+ this.activeTask_ = undefined;
+ }.bind(this));
+ }
+ },
+
+ /**
+ * Updates the filter hits based on the provided |filterText|. Returns a
+ * promise which resolves when |filterHits| has been refreshed.
+ */
+ startFiltering(filterText) {
+ const sc = this.brushingStateController_;
+ if (!sc) return;
+
+ // TODO(beaudoin): Cancel anything left in the task queue, without
+ // invalidating the promise.
+ this.enqueueOperation_(function() {
+ this.filterHits_ = [];
+ this.currentHitIndex_ = -1;
+ }.bind(this));
+
+ // Try constructing a UIState from the filterText.
+ // UIState.fromUserFriendlyString will throw an error only if the string
+ // is syntactically correct to a UI state string but with invalid values.
+ // It will return undefined if there is no syntactic match.
+ let stateFromString;
+ try {
+ stateFromString = sc.uiStateFromString(filterText);
+ } catch (e) {
+ this.enqueueOperation_(function() {
+ const overlay = new tr.ui.b.Overlay();
+ Polymer.dom(overlay).textContent = e.message;
+ overlay.title = 'UI State Navigation Error';
+ overlay.visible = true;
+ });
+ return this.activePromise_;
+ }
+
+ if (stateFromString !== undefined) {
+ this.enqueueOperation_(
+ sc.navToPosition.bind(this, stateFromString, true));
+ } else {
+ // filterText is not a navString here -- proceed with find and filter.
+ if (filterText.length === 0) {
+ this.enqueueOperation_(sc.findTextCleared.bind(sc));
+ } else {
+ const filter = new tr.c.FullTextFilter(filterText);
+ const filterHitSet = new tr.model.EventSet();
+ this.enqueueOperation_(sc.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, filterHitSet));
+ this.enqueueOperation_(function() {
+ this.filterHits_ = filterHitSet.toArray();
+ sc.findTextChangedTo(filterHitSet);
+ }.bind(this));
+ }
+ }
+ return this.activePromise_;
+ },
+
+ /**
+ * Returns the most recent filter hits as an array. Call
+ * |startFiltering| to ensure this is up to date after the filter settings
+ * have been changed.
+ */
+ get filterHits() {
+ return this.filterHits_;
+ },
+
+ get currentHitIndex() {
+ return this.currentHitIndex_;
+ },
+
+ find_(dir) {
+ const firstHit = this.currentHitIndex_ === -1;
+ if (firstHit && dir < 0) {
+ this.currentHitIndex_ = 0;
+ }
+
+ const N = this.filterHits.length;
+ this.currentHitIndex_ = (this.currentHitIndex_ + dir + N) % N;
+
+ if (!this.brushingStateController_) return;
+
+ this.brushingStateController_.findFocusChangedTo(
+ new tr.model.EventSet(this.filterHits[this.currentHitIndex]));
+ },
+
+ findNext() {
+ this.find_(1);
+ },
+
+ findPrevious() {
+ this.find_(-1);
+ }
+ };
+
+ return {
+ FindController,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html
new file mode 100644
index 00000000000..76f3362899b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/find_controller_test.html
@@ -0,0 +1,366 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/find_controller.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Task = tr.b.Task;
+
+ /*
+ * Just enough of the BrushingStateController to support the tests below.
+ */
+ function FakeBrushingStateController() {
+ this.addAllEventsMatchingFilterToSelectionReturnValue = [];
+
+ this.viewport = undefined;
+ this.model = undefined;
+ this.selection = new tr.model.EventSet();
+ this.findMatches = new tr.model.EventSet();
+ }
+
+ FakeBrushingStateController.prototype = {
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ return new Task(function() {
+ const n = this.addAllEventsMatchingFilterToSelectionReturnValue.length;
+ for (let i = 0; i < n; i++) {
+ selection.push(
+ this.addAllEventsMatchingFilterToSelectionReturnValue[i]);
+ }
+ }, this);
+ },
+
+ uiStateFromString(string) {
+ return undefined;
+ },
+
+ findTextChangedTo(selection) {
+ this.findMatches = selection;
+ this.selection = new tr.model.EventSet();
+ },
+
+ findFocusChangedTo(selection) {
+ this.selection = selection;
+ },
+
+ findTextCleared(selection) {
+ this.selection = new tr.model.EventSet();
+ this.findMatches = new tr.model.EventSet();
+ }
+ };
+
+ test('findControllerNoModel', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+ controller.findNext();
+ controller.findPrevious();
+ });
+
+ test('findControllerEmptyHit', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ brushingStateController.selection = new tr.model.EventSet();
+ brushingStateController.findMatches = new tr.model.EventSet();
+ controller.findNext();
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.lengthOf(brushingStateController.findMatches, 0);
+ controller.findPrevious();
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.lengthOf(brushingStateController.findMatches, 0);
+ });
+
+ test('findControllerOneHit', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ const s1 = {guid: 1};
+ brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [
+ s1
+ ];
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('asdf').then(function() {
+ try {
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.findMatches), s1);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.findMatches), s1);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.findMatches), s1);
+
+ controller.findPrevious();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.findMatches), s1);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+
+ test('findControllerMultipleHits', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ const s1 = {guid: 1};
+ const s2 = {guid: 2};
+ const s3 = {guid: 3};
+
+ brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [
+ s1, s2, s3
+ ];
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('asdf').then(function() {
+ try {
+ // Loop through hits then when we wrap, try moving backward.
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.lengthOf(brushingStateController.findMatches, 3);
+ let matches = Array.from(brushingStateController.findMatches);
+ assert.strictEqual(matches[0], s1);
+ assert.strictEqual(matches[1], s2);
+ assert.strictEqual(matches[2], s3);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s2);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s3);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+
+ controller.findPrevious();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s3);
+
+ controller.findPrevious();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s2);
+ assert.lengthOf(brushingStateController.findMatches, 3);
+ matches = Array.from(brushingStateController.findMatches);
+ assert.strictEqual(matches[0], s1);
+ assert.strictEqual(matches[1], s2);
+ assert.strictEqual(matches[2], s3);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+
+ test('findControllerChangeFilterAfterNext', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ const s1 = {guid: 1};
+ const s2 = {guid: 2};
+ const s3 = {guid: 3};
+ const s4 = {guid: 4};
+
+ brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [
+ s1, s2, s3
+ ];
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('asdf').then(function() {
+ // Loop through hits then when we wrap, try moving backward.
+ controller.findNext();
+ brushingStateController.
+ addAllEventsMatchingFilterToSelectionReturnValue = [s4];
+
+ controller.startFiltering('asdfsf').then(function() {
+ controller.findNext();
+ try {
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s4);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+ });
+
+ test('findControllerSelectsAllItemsFirst', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ const s1 = {guid: 1};
+ const s2 = {guid: 2};
+ const s3 = {guid: 3};
+ brushingStateController.addAllEventsMatchingFilterToSelectionReturnValue = [
+ s1, s2, s3
+ ];
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('asdfsf').then(function() {
+ try {
+ assert.lengthOf(brushingStateController.selection, 0);
+ assert.lengthOf(brushingStateController.findMatches, 3);
+ let matches = Array.from(brushingStateController.findMatches);
+ assert.strictEqual(matches[0], s1);
+ assert.strictEqual(matches[1], s2);
+ assert.strictEqual(matches[2], s3);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s1);
+
+ controller.findNext();
+ assert.strictEqual(
+ tr.b.getOnlyElement(brushingStateController.selection), s2);
+ assert.lengthOf(brushingStateController.findMatches, 3);
+ matches = Array.from(brushingStateController.findMatches);
+ assert.strictEqual(matches[0], s1);
+ assert.strictEqual(matches[1], s2);
+ assert.strictEqual(matches[2], s3);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+
+ test('findControllerWithRealTimeline', function() {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ '', 'a', 0, 1, {}, 3));
+ model.t1 = t1;
+ });
+
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+
+ const timeline = document.createElement('tr-ui-timeline-view');
+ Polymer.dom(timeline).appendChild(container);
+
+ // This is for testing only, have to make sure things link up right.
+ timeline.trackViewContainer_ = container;
+
+ timeline.model = model;
+
+ const brushingStateController = timeline.brushingStateController;
+ const controller = timeline.findCtl_.controller;
+
+ // Test find with no filterText.
+ controller.findNext();
+
+ // Test find with filter txt.
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('a').then(function() {
+ try {
+ assert.strictEqual(brushingStateController.selection.length, 0);
+ assert.deepEqual(Array.from(brushingStateController.findMatches),
+ model.t1.sliceGroup.slices);
+
+ controller.findNext();
+ assert.isTrue(brushingStateController.selection.equals(
+ new tr.model.EventSet(model.t1.sliceGroup.slices[0])));
+
+ controller.startFiltering('xxx').then(function() {
+ try {
+ assert.strictEqual(brushingStateController.findMatches.length, 0);
+ assert.strictEqual(brushingStateController.selection.length, 1);
+
+ controller.findNext();
+ assert.strictEqual(brushingStateController.selection.length, 0);
+
+ controller.findNext();
+ assert.strictEqual(brushingStateController.selection.length, 0);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+
+ test('findControllerNavigation', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+
+ let navToPositionCallCount = 0;
+ let findTextClearedCallCount = 0;
+ const fakeUIState = {};
+ brushingStateController.uiStateFromString = function(string) {
+ if (string === '') return undefined;
+
+ assert.strictEqual(string, '2000@1.2x7');
+ return fakeUIState;
+ };
+ brushingStateController.navToPosition = function(uiState) {
+ assert.strictEqual(uiState, fakeUIState);
+ navToPositionCallCount++;
+ };
+ brushingStateController.findTextCleared = function() {
+ findTextClearedCallCount++;
+ };
+
+ return new Promise(function(resolve, reject) {
+ controller.startFiltering('2000@1.2x7').then(function() {
+ assert.strictEqual(navToPositionCallCount, 1);
+ }).then(function() {
+ controller.startFiltering('').then(function() {
+ try {
+ assert.strictEqual(findTextClearedCallCount, 1);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ });
+ });
+
+ test('findControllerClearAfterSet', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ const controller = new tr.ui.FindController(brushingStateController);
+ let findTextChangedToCalled = false;
+ brushingStateController.findTextChangedTo = function(selection) {
+ findTextChangedToCalled = true;
+ };
+ brushingStateController.findTextCleared = function() {
+ assert.strictEqual(findTextChangedToCalled, true);
+ };
+ controller.startFiltering('1');
+ controller.startFiltering('');
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png
new file mode 100644
index 00000000000..8eef2bf7ecc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-left.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png
new file mode 100644
index 00000000000..c67e697de5f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-mid.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png
new file mode 100644
index 00000000000..834004a0f74
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/images/chrome-right.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png b/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png
new file mode 100644
index 00000000000..83d09179817
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/images/ui-states.png
Binary files differ
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html b/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html
new file mode 100644
index 00000000000..2f21d5156f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/metrics_debugger_app.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/extras/full_config.html">
+<link rel="import" href="/tracing/importer/import.html">
+<link rel="import" href="/tracing/metrics/all_metrics.html">
+<link rel="import" href="/tracing/metrics/metric_map_function.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/mre/mre_result.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/file.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<dom-module id='tracing-ui-metrics-debugger-app'>
+ <style>
+ pre {
+ overflow: auto;
+ }
+ #bar {
+ display: flex;
+ flex-direction: row;
+ padding: 1px 6px;
+ }
+ </style>
+
+ <template>
+ <top-left-controls id="top_left_controls"></top-left-controls>
+ <input id="load_trace" type="file"/>
+ <button id="run_metric">Run metric</button>
+ <div id="trace_info"></div>
+ <pre id="map_results">
+ </pre>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ Polymer({
+ is: 'tracing-ui-metrics-debugger-app',
+ created() {
+ this.metrics_ = [];
+ tr.metrics.MetricRegistry.getAllRegisteredTypeInfos().forEach(
+ function(m) {
+ this.metrics_.push({
+ label: m.constructor.name,
+ value: m.constructor.name
+ });
+ }, this);
+ this.activeTrace_ = undefined;
+ this.settingsKey_ = undefined;
+ this.currentMetricName_ = undefined;
+ this.settingsKey_ = 'metrics-debugger-app-metric-name';
+ },
+
+ ready() {
+ const metricSelector = tr.ui.b.createSelector(
+ this, 'currentMetricName_',
+ this.settingsKey_,
+ this.metrics_[0].value,
+ this.metrics_);
+ Polymer.dom(this.$.top_left_controls).appendChild(
+ metricSelector);
+
+ this.$.load_trace.addEventListener('change', function(event) {
+ const file = event.target.files[0];
+ this.onTraceFileSelected_(file);
+ }.bind(this));
+ this.$.run_metric.addEventListener(
+ 'click', function(event) {
+ event.stopPropagation();
+ this.onRunMetricClicked_();
+ }.bind(this));
+ },
+
+ onRunMetricClicked_() {
+ if (this.activeTrace_ === undefined) {
+ tr.ui.b.Overlay.showError('You must load a trace first!');
+ return;
+ }
+ const result = new tr.mre.MreResult();
+ const model = this.activeTrace_.model;
+ const options = {metrics: [this.currentMetricName_]};
+ try {
+ tr.metrics.metricMapFunction(result, model, options);
+ this.set(
+ '$.map_results.textContent',
+ 'Metric result:\n' + JSON.stringify(result.asDict(), undefined, 2));
+ } catch (err) {
+ tr.ui.b.Overlay.showError('Error running metric:\n' + err.stack);
+ }
+ },
+
+ onTraceFileSelected_(file) {
+ tr.ui.b.readFile(file).then(
+ function(data) {
+ this.setActiveTrace(file.name, data);
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('Error while loading file: ' + err);
+ });
+ },
+
+ setActiveTrace(filename, data) {
+ const model = new tr.Model();
+ const importOptions = new tr.importer.ImportOptions();
+ importOptions.pruneEmptyContainers = false;
+ importOptions.showImportWarnings = true;
+ importOptions.trackDetailedModelStats = true;
+
+ const i = new tr.importer.Import(model, importOptions);
+ i.importTracesWithProgressDialog([data]).then(
+ function() {
+ this.activeTrace_ = {
+ filename,
+ model,
+ };
+ Polymer.dom(this.$.trace_info).textContent = 'Trace file ' +
+ filename + ' is loaded.';
+ }.bind(this),
+ function(err) {
+ tr.ui.b.Overlay.showError('Trace import error: ' + err);
+ });
+ },
+ });
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html
new file mode 100644
index 00000000000..2ab621151db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/null_brushing_state_controller.html
@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event_target.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/base/ui_state.html">
+<link rel="import" href="/tracing/ui/brushing_state.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ /*
+ * Some elements such as analysis-links require at least one of their
+ * ancestors to have a BrushingStateController.
+ * Some clients of such elements, such as histogram-set-view, do not have a
+ * timeline-view, which is required by the BrushingStateController.
+ * This class provides the API of BrushingStateController but not the
+ * implementation, unless there is a real BrushingStateController in the
+ * owning element's ancestor chain, in which case the implementation is
+ * delegated to the real BrushingStateController.
+ */
+ class NullBrushingStateController extends tr.c.BrushingStateController {
+ constructor() {
+ super(undefined);
+ this.parentController = undefined;
+ }
+
+ dispatchChangeEvent_() {
+ if (this.parentController) this.parentController.dispatchChangeEvent_();
+ }
+
+ get model() {
+ if (!this.parentController) return undefined;
+ return this.parentController.model;
+ }
+
+ get trackView() {
+ if (!this.parentController) return undefined;
+ return this.parentController.trackView;
+ }
+
+ get viewport() {
+ if (!this.parentController) return undefined;
+ return this.parentController.viewport;
+ }
+
+ get historyEnabled() {
+ if (!this.parentController) return undefined;
+ return this.parentController.historyEnabled;
+ }
+
+ set historyEnabled(historyEnabled) {
+ if (this.parentController) {
+ this.parentController.historyEnabled = historyEnabled;
+ }
+ }
+
+ modelWillChange() {
+ if (this.parentController) this.parentController.modelWillChange();
+ }
+
+ modelDidChange() {
+ if (this.parentController) this.parentController.modelDidChange();
+ }
+
+ onUserInitiatedSelectionChange_() {
+ if (this.parentController) {
+ this.parentController.onUserInitiatedSelectionChange_();
+ }
+ }
+
+ onPopState_(e) {
+ if (this.parentController) this.parentController.onPopState_(e);
+ }
+
+ get selection() {
+ if (!this.parentController) return undefined;
+ return this.parentController.selection;
+ }
+
+ get findMatches() {
+ if (!this.parentController) return undefined;
+ return this.parentController.findMatches;
+ }
+
+ get selectionOfInterest() {
+ if (!this.parentController) return undefined;
+ return this.parentController.selectionOfInterest;
+ }
+
+ get currentBrushingState() {
+ if (!this.parentController) return undefined;
+ return this.parentController.currentBrushingState;
+ }
+
+ set currentBrushingState(newBrushingState) {
+ if (this.parentController) {
+ this.parentController.currentBrushingState = newBrushingState;
+ }
+ }
+
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ if (this.parentController) {
+ this.parentController.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, selection);
+ }
+ }
+
+ findTextChangedTo(allPossibleMatches) {
+ if (this.parentController) {
+ this.parentController.findTextChangedTo(allPossibleMatches);
+ }
+ }
+
+ findFocusChangedTo(currentFocus) {
+ if (this.parentController) {
+ this.parentController.findFocusChangedTo(currentFocus);
+ }
+ }
+
+ findTextCleared() {
+ if (this.parentController) {
+ this.parentController.findTextCleared();
+ }
+ }
+
+ uiStateFromString(string) {
+ if (this.parentController) {
+ this.parentController.uiStateFromString(string);
+ }
+ }
+
+ navToPosition(uiState, showNavLine) {
+ if (this.parentController) {
+ this.parentController.navToPosition(uiState, showNavLine);
+ }
+ }
+
+ changeSelectionFromTimeline(selection) {
+ if (this.parentController) {
+ this.parentController.changeSelectionFromTimeline(selection);
+ }
+ }
+
+ showScriptControlSelection(selection) {
+ if (this.parentController) {
+ this.parentController.showScriptControlSelection(selection);
+ }
+ }
+
+ changeSelectionFromRequestSelectionChangeEvent(selection) {
+ if (this.parentController) {
+ this.parentController.changeSelectionFromRequestSelectionChangeEvent(
+ selection);
+ }
+ }
+
+ changeAnalysisViewRelatedEvents(eventSet) {
+ if (this.parentController && (eventSet instanceof tr.model.EventSet)) {
+ this.parentController.changeAnalysisViewRelatedEvents(eventSet);
+ }
+ }
+
+ changeAnalysisLinkHoveredEvents(eventSet) {
+ if (this.parentController && (eventSet instanceof tr.model.EventSet)) {
+ this.parentController.changeAnalysisLinkHoveredEvents(eventSet);
+ }
+ }
+
+ getViewSpecificBrushingState(viewId) {
+ if (this.parentController) {
+ this.parentController.getViewSpecificBrushingState(viewId);
+ }
+ }
+
+ changeViewSpecificBrushingState(viewId, newState) {
+ if (this.parentController) {
+ this.parentController.changeViewSpecificBrushingState(viewId, newState);
+ }
+ }
+ }
+
+ return {
+ NullBrushingStateController,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html
new file mode 100644
index 00000000000..a91a81e05ec
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/extras/tquery/tquery.html">
+
+<dom-module id='tr-ui-scripting-control'>
+ <template>
+ <style>
+ :host {
+ flex: 1 1 auto;
+ }
+ .root {
+ font-family: monospace;
+ cursor: text;
+
+ padding: 2px;
+ margin: 2px;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ background: white;
+
+ height: 100px;
+ overflow-y: auto;
+
+ transition-property: opacity, height, padding, margin;
+ transition-duration: .2s;
+ transition-timing-function: ease-out;
+ }
+ .hidden {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ height: 0px;
+ opacity: 0;
+ }
+ .focused {
+ outline: auto 5px -webkit-focus-ring-color;
+ }
+ #history {
+ -webkit-user-select: text;
+ color: #777;
+ }
+ #promptContainer {
+ display: flex;
+ }
+ #promptMark {
+ width: 1em;
+ color: #468;
+ }
+ #prompt {
+ flex: 1;
+ width: 100%;
+ border: none !important;
+ background-color: inherit !important;
+ font: inherit !important;
+ text-overflow: clip !important;
+ text-decoration: none !important;
+ }
+ #prompt:focus {
+ outline: none;
+ }
+ </style>
+
+ <div id="root" class="root hidden" tabindex="0"
+ on-focus="onConsoleFocus">
+ <div id='history'></div>
+ <div id='promptContainer'>
+ <span id='promptMark'>&gt;</span>
+ <input id='prompt' type='text'
+ on-keypress="promptKeyPress"
+ on-keydown="promptKeyDown"
+ on-blur="onConsoleBlur">
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-scripting-control',
+
+ isEnterKey_(event) {
+ // Check if in IME.
+ // Remove keyIdentifier after reference build rolls past M51 when
+ // KeyboardEvent.key was added.
+ return event.keyCode !== 229 &&
+ (event.key === 'Enter' || event.keyIdentifier === 'Enter');
+ },
+
+ setFocus_(focused) {
+ const promptEl = this.$.prompt;
+ if (focused) {
+ promptEl.focus();
+ Polymer.dom(this.$.root).classList.add('focused');
+ // Move cursor to the end of any existing text.
+ if (promptEl.value.length > 0) {
+ const sel = window.getSelection();
+ sel.collapse(
+ Polymer.dom(promptEl).firstChild, promptEl.value.length);
+ }
+ } else {
+ promptEl.blur();
+ Polymer.dom(this.$.root).classList.remove('focused');
+ // Workaround for crbug.com/89026 to ensure the prompt doesn't retain
+ // keyboard focus.
+ const parent = promptEl.parentElement;
+ const nextEl = Polymer.dom(promptEl).nextSibling;
+ promptEl.remove();
+ Polymer.dom(parent).insertBefore(promptEl, nextEl);
+ }
+ },
+
+ onConsoleFocus(e) {
+ e.stopPropagation();
+ this.setFocus_(true);
+ },
+
+ onConsoleBlur(e) {
+ e.stopPropagation();
+ this.setFocus_(false);
+ },
+
+ promptKeyDown(e) {
+ e.stopPropagation();
+ if (!this.isEnterKey_(e)) return;
+
+ e.preventDefault();
+ const promptEl = this.$.prompt;
+ const command = promptEl.value;
+ if (command.length === 0) return;
+
+ promptEl.value = '';
+ this.addLine_(String.fromCharCode(187) + ' ' + command);
+
+ let result;
+ try {
+ result = this.controller_.executeCommand(command);
+ } catch (e) {
+ result = e.stack || e.stackTrace;
+ }
+
+ if (result instanceof tr.e.tquery.TQuery) {
+ // TODO(skyostil): Show a cool spinner.
+ result.ready().then(function(selection) {
+ this.addLine_(selection.length + ' matches');
+ this.controller_.brushingStateController.
+ showScriptControlSelection(selection);
+ }.bind(this));
+ } else {
+ this.addLine_(result);
+ }
+ promptEl.scrollIntoView();
+ },
+
+ addLine_(line) {
+ const historyEl = this.$.history;
+ if (historyEl.innerText.length !== 0) {
+ historyEl.innerText += '\n';
+ }
+ historyEl.innerText += line;
+ },
+
+ promptKeyPress(e) {
+ e.stopPropagation();
+ },
+
+ toggleVisibility() {
+ const root = this.$.root;
+ if (!this.visible) {
+ Polymer.dom(root).classList.remove('hidden');
+ this.setFocus_(true);
+ } else {
+ Polymer.dom(root).classList.add('hidden');
+ this.setFocus_(false);
+ }
+ },
+
+ get hasFocus() {
+ return this === document.activeElement;
+ },
+
+ get visible() {
+ const root = this.$.root;
+ return !Polymer.dom(root).classList.contains('hidden');
+ },
+
+ get controller() {
+ return this.controller_;
+ },
+
+ set controller(c) {
+ this.controller_ = c;
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html
new file mode 100644
index 00000000000..69e8b2b9de7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/scripting_control_test.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/scripting_control.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const ctl = document.createElement('tr-ui-scripting-control');
+ this.addHTMLOutput(ctl);
+ ctl.toggleVisibility();
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html
new file mode 100644
index 00000000000..b2611f4ac3a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/statistics.html">
+<link rel="import" href="/tracing/base/scalar.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/ui/base/grouping_table.html">
+<link rel="import" href="/tracing/ui/base/grouping_table_groupby_picker.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+<link rel="import" href="/tracing/value/ui/scalar_span.html">
+
+<dom-module id='tr-ui-sp-file-size-stats-side-panel'>
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ toolbar {
+ align-items: center;
+ background-color: rgb(236, 236, 236);
+ border-bottom: 1px solid #8e8e8e;
+ display: flex;
+ flex-direction: row;
+ flex-direction: row;
+ flex: 0 0 auto;
+ font-size: 12px;
+ padding: 0 10px 0 10px;
+ }
+ table-container {
+ display: flex;
+ min-height: 0px;
+ overflow-y: auto;
+ }
+ </style>
+
+ <toolbar>
+ <span><b>Group by:</b></span>
+ <tr-ui-b-grouping-table-groupby-picker id="picker">
+ </tr-ui-b-grouping-table-groupby-picker>
+ </toolbar>
+ <table-container>
+ <tr-ui-b-grouping-table id="table"></tr-ui-b-grouping-table>
+ </table-container>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+(function() {
+ Polymer({
+ is: 'tr-ui-sp-file-size-stats-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+ ready() {
+ this.model_ = undefined;
+ this.selection_ = new tr.model.EventSet();
+ this.$.picker.settingsKey = 'tr-ui-sp-file-size-stats-side-panel-picker';
+ this.$.picker.possibleGroups = [
+ {
+ key: 'phase', label: 'Event Type',
+ dataFn(eventStat) { return eventStat.phase; }
+ },
+ {
+ key: 'category', label: 'Category',
+ dataFn(eventStat) { return eventStat.category; }
+ },
+ {
+ key: 'title', label: 'Title',
+ dataFn(eventStat) { return eventStat.title; }
+ }
+ ];
+ // If the picker did not restore currentGroupKeys from Settings,
+ // then set default currentGroupKeys.
+ if (this.$.picker.currentGroupKeys.length === 0) {
+ this.$.picker.currentGroupKeys = ['phase', 'title'];
+ }
+ this.$.picker.addEventListener('current-groups-changed',
+ this.updateContents_.bind(this));
+ },
+
+ get textLabel() {
+ return 'File Size Stats';
+ },
+
+ supportsModel(m) {
+ if (!m) {
+ return {
+ supported: false,
+ reason: 'No stats were collected for this file.'
+ };
+ }
+
+ if (m.stats.allTraceEventStats.length === 0) {
+ return {
+ supported: false,
+ reason: 'No stats were collected for this file.'
+ };
+ }
+ return {
+ supported: true
+ };
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ this.rangeOfInterest_ = rangeOfInterest;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ this.selection_ = selection;
+ },
+
+ createColumns_(stats) {
+ const columns = [
+ {
+ title: 'Title',
+ value(row) {
+ const titleEl = document.createElement('span');
+ Polymer.dom(titleEl).textContent = row.title;
+ titleEl.style.textOverflow = 'ellipsis';
+ return titleEl;
+ },
+ cmp(a, b) {
+ return a.title.localeCompare(b.title);
+ },
+ width: '400px'
+ },
+ {
+ title: 'Num Events',
+ align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT,
+ value(row) {
+ return row.rowStats.numEvents;
+ },
+ cmp(a, b) {
+ return a.rowStats.numEvents - b.rowStats.numEvents;
+ },
+ width: '80px'
+ }
+ ];
+
+ if (stats && stats.hasEventSizesinBytes) {
+ columns.push({
+ title: 'Bytes',
+ value(row) {
+ const value = new tr.b.Scalar(tr.b.Unit.byName.sizeInBytes,
+ row.rowStats.totalEventSizeinBytes);
+ const spanEl = tr.v.ui.createScalarSpan(value);
+ return spanEl;
+ },
+ cmp(a, b) {
+ return a.rowStats.totalEventSizeinBytes -
+ b.rowStats.totalEventSizeinBytes;
+ },
+ width: '80px'
+ });
+ }
+ return columns;
+ },
+
+ updateContents_() {
+ const table = this.$.table;
+
+ const columns = this.createColumns_(this.model.stats);
+ table.rowStatsConstructor = function ModelStatsRowStats(row) {
+ const sum = tr.b.math.Statistics.sum(row.data, function(x) {
+ return x.numEvents;
+ });
+ const totalEventSizeinBytes = tr.b.math.Statistics.sum(row.data, x =>
+ x.totalEventSizeinBytes
+ );
+ return {
+ numEvents: sum,
+ totalEventSizeinBytes
+ };
+ };
+ table.tableColumns = columns;
+ table.sortColumnIndex = 1;
+ table.sortDescending = true;
+ table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
+
+ table.groupBy = this.$.picker.currentGroups.map(function(group) {
+ return group.dataFn;
+ });
+
+ if (!this.model) {
+ table.dataToGroup = [];
+ } else {
+ table.dataToGroup = this.model.stats.allTraceEventStats;
+ }
+ this.$.table.rebuild();
+ }
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-sp-file-size-stats-side-panel');
+ });
+})();
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html
new file mode 100644
index 00000000000..a764387427a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/file_size_stats_side_panel_test.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/side_panel/file_size_stats_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TestUtils = tr.c.TestUtils;
+
+ function createModel(opt_customizeModelCallback) {
+ return TestUtils.newModel(function(model) {
+ const modelStats = model.stats;
+ modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1');
+ modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1');
+ modelStats.willProcessBasicTraceEvent('X', 'cat2', 'title1');
+ modelStats.willProcessBasicTraceEvent('X', 'cat2', 'title3');
+ modelStats.willProcessBasicTraceEvent('Y', 'cat3', 'title3');
+ });
+ }
+
+ test('instantiate', function() {
+ const panel = document.createElement('tr-ui-sp-file-size-stats-side-panel');
+ panel.model = createModel();
+ panel.style.height = '200px';
+ this.addHTMLOutput(panel);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html
new file mode 100644
index 00000000000..4508a4347a9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/metrics/metric_map_function.html">
+<link rel="import" href="/tracing/metrics/metric_registry.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/mre/mre_result.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+<link rel="import" href="/tracing/value/histogram_set.html">
+<link rel="import" href="/tracing/value/ui/histogram_set_view.html">
+
+<dom-module id="tr-ui-sp-metrics-side-panel">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ div#error {
+ color: red;
+ }
+ #results {
+ font-size: 12px;
+ }
+ </style>
+
+ <top-left-controls id="top_left_controls"></top-left-controls>
+
+ <tr-v-ui-histogram-set-view id="results"></tr-v-ui-histogram-set-view>
+
+ <div id="error"></div>
+ </template>
+</dom-module>
+
+<script>
+'use strict';
+tr.exportTo('tr.ui', function() {
+ Polymer({
+ is: 'tr-ui-sp-metrics-side-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+
+ ready() {
+ this.model_ = undefined;
+
+ this.rangeOfInterest_ = undefined;
+ this.metricLatenciesMs_ = [];
+
+ this.metrics_ = [];
+ tr.metrics.MetricRegistry.getAllRegisteredTypeInfos().forEach(
+ function(m) {
+ if (m.constructor.name === 'sampleMetric') return;
+
+ this.metrics_.push({
+ label: m.constructor.name,
+ value: m.constructor.name
+ });
+ }, this);
+
+ this.metrics_.sort((x, y) => x.label.localeCompare(y.label));
+
+ this.settingsKey_ = 'metrics-side-panel-metric-name';
+ this.currentMetricName_ = 'responsivenessMetric';
+ const metricSelector = tr.ui.b.createSelector(
+ this, 'currentMetricName_',
+ this.settingsKey_,
+ this.currentMetricName_,
+ this.metrics_);
+ Polymer.dom(this.$.top_left_controls).appendChild(metricSelector);
+ metricSelector.addEventListener('change',
+ this.onMetricChange_.bind(this));
+ this.currentMetricTypeInfo_ =
+ tr.metrics.MetricRegistry.findTypeInfoWithName(
+ this.currentMetricName_);
+
+ this.recomputeButton_ = tr.ui.b.createButton(
+ 'Recompute', this.onRecompute_, this);
+ Polymer.dom(this.$.top_left_controls).appendChild(this.recomputeButton_);
+
+ this.$.results.addEventListener('display-ready', () => {
+ this.$.results.style.display = '';
+ });
+ },
+
+ async build(model) {
+ this.model_ = model;
+ await this.updateContents_();
+ },
+
+ /**
+ * Return an estimate of how many milliseconds it would take to re-run the
+ * metric. If the metric has not been run, return undefined.
+ *
+ * @return {undefined|number}
+ */
+ get metricLatencyMs() {
+ return tr.b.math.Statistics.mean(this.metricLatenciesMs_);
+ },
+
+ onMetricChange_() {
+ this.currentMetricTypeInfo_ =
+ tr.metrics.MetricRegistry.findTypeInfoWithName(
+ this.currentMetricName_);
+ this.metricLatenciesMs_ = [];
+ this.updateContents_();
+ },
+
+ onRecompute_() {
+ this.updateContents_();
+ },
+
+ get textLabel() {
+ return 'Metrics';
+ },
+
+ supportsModel(m) {
+ if (!m) {
+ return {
+ supported: false,
+ reason: 'No model available'
+ };
+ }
+
+ return {
+ supported: true
+ };
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.build(model);
+ },
+
+ get selection() {
+ // Not applicable to metrics.
+ },
+
+ set selection(_) {
+ // Not applicable to metrics.
+ },
+
+ /**
+ * @return {undefined|!tr.b.math.Range}
+ */
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ /**
+ * This may be called rapidly as the mouse is moved.
+ * If the metric supportsRangeOfInterest and takes less than 100ms, then it
+ * will be re-run immediately; otherwise, the Recompute button will be
+ * enabled.
+ *
+ * @param {!tr.b.math.Range} range
+ */
+ set rangeOfInterest(range) {
+ this.rangeOfInterest_ = range;
+
+ if (this.currentMetricTypeInfo_ &&
+ this.currentMetricTypeInfo_.metadata.supportsRangeOfInterest) {
+ if ((this.metricLatencyMs === undefined) ||
+ (this.metricLatencyMs < 100)) {
+ this.updateContents_();
+ } else {
+ this.recomputeButton_.style.background = 'red';
+ }
+ }
+ },
+
+ async updateContents_() {
+ Polymer.dom(this.$.error).textContent = '';
+ this.$.results.style.display = 'none';
+
+ if (!this.model_) {
+ Polymer.dom(this.$.error).textContent = 'Missing model';
+ return;
+ }
+
+ const options = {metrics: [this.currentMetricName_]};
+
+ if (this.currentMetricTypeInfo_ &&
+ this.currentMetricTypeInfo_.metadata.supportsRangeOfInterest &&
+ this.rangeOfInterest &&
+ !this.rangeOfInterest.isEmpty) {
+ options.rangeOfInterest = this.rangeOfInterest;
+ }
+
+ const startDate = new Date();
+ const addFailureCb = failure => {
+ Polymer.dom(this.$.error).textContent = failure.description;
+ };
+ const histograms = tr.metrics.runMetrics(
+ this.model_, options, addFailureCb);
+
+ this.metricLatenciesMs_.push(new Date() - startDate);
+ while (this.metricLatenciesMs_.length > 20) {
+ this.metricLatenciesMs_.shift();
+ }
+
+ this.recomputeButton_.style.background = '';
+
+ await this.$.results.build(histograms);
+ }
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function() {
+ return document.createElement('tr-ui-sp-metrics-side-panel');
+ });
+
+ return {};
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html
new file mode 100644
index 00000000000..dc90ab54930
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/metrics_side_panel_test.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/side_panel/metrics_side_panel.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function createModel() {
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ const browserProcess = m.getOrCreateProcess(1);
+ const browserMain = browserProcess.getOrCreateThread(2);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 0);
+ browserMain.sliceGroup.endSlice(10);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 20);
+ browserMain.sliceGroup.endSlice(30);
+ }
+ });
+ return m;
+ }
+
+ function testMetric(values, model) {
+ const hist = new tr.v.Histogram('test histogram', tr.b.Unit.byName.count);
+ hist.addSample(1);
+ values.addHistogram(hist);
+ }
+
+ tr.metrics.MetricRegistry.register(testMetric);
+
+ test('instantiateCollapsed', async function() {
+ const metricsPanel = document.createElement('tr-ui-sp-metrics-side-panel');
+ this.addHTMLOutput(metricsPanel);
+ metricsPanel.currentMetricName_ = 'testMetric';
+ await metricsPanel.build(createModel());
+
+ assert.isDefined(tr.ui.b.findDeepElementMatchingPredicate(
+ metricsPanel, elem => elem.textContent === 'test histogram'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html
new file mode 100644
index 00000000000..8b8e2f52b14
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.behaviors', function() {
+ const SidePanel = {
+
+ get rangeOfInterest() {
+ throw new Error('Not implemented');
+ },
+
+ set rangeOfInterest(rangeOfInterest) {
+ throw new Error('Not implemented');
+ },
+
+ get selection() {
+ throw new Error('Not implemented');
+ },
+
+ set selection(selection) {
+ throw new Error('Not implemented');
+ },
+
+ get model() {
+ throw new Error('Not implemented');
+ },
+
+ set model(model) {
+ throw new Error('Not implemented');
+ },
+
+ supportsModel(m) {
+ throw new Error('Not implemented');
+ }
+ };
+
+ return {
+ SidePanel,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html
new file mode 100644
index 00000000000..95be3103873
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container.html
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<dom-module id='tr-ui-side-panel-container'>
+ <template>
+ <style>
+ :host {
+ align-items: stretch;
+ display: flex;
+ background-color: white;
+ }
+
+ :host([expanded]) > #side_panel_drag_handle,
+ :host([expanded]) > active-panel-container {
+ flex: 1 1 auto;
+ border-left: 1px solid black;
+ display: flex;
+ }
+
+ :host(:not([expanded])) > #side_panel_drag_handle,
+ :host(:not([expanded])) > active-panel-container {
+ display: none;
+ }
+
+ active-panel-container {
+ display: flex;
+ }
+
+ tab-strip {
+ flex: 0 0 auto;
+ flex-direction: column;
+ -webkit-user-select: none;
+ background-color: rgb(236, 236, 236);
+ border-left: 1px solid black;
+ cursor: default;
+ display: flex;
+ min-width: 18px; /* workaround for flexbox and writing-mode mixing bug */
+ padding: 10px 0 10px 0;
+ font-size: 12px;
+ }
+
+ tab-strip > tab-strip-label {
+ flex-shrink: 0;
+ -webkit-writing-mode: vertical-rl;
+ white-space: nowrap;
+ display: inline;
+ margin-right: 1px;
+ min-height: 20px;
+ padding: 15px 3px 15px 1px;
+ }
+
+ tab-strip >
+ tab-strip-label:not([enabled]) {
+ color: rgb(128, 128, 128);
+ }
+
+ tab-strip > tab-strip-label[selected] {
+ background-color: white;
+ border: 1px solid rgb(163, 163, 163);
+ border-left: none;
+ padding: 14px 2px 14px 1px;
+ }
+
+ #active_panel_container {
+ overflow: auto;
+ }
+ </style>
+
+ <tr-ui-b-drag-handle id="side_panel_drag_handle"></tr-ui-b-drag-handle>
+ <active-panel-container id='active_panel_container'>
+ </active-panel-container>
+ <tab-strip id='tab_strip'></tab-strip>
+ </template>
+</dom-module>
+<script>
+'use strict';
+Polymer({
+ is: 'tr-ui-side-panel-container',
+
+ ready() {
+ this.activePanelContainer_ = this.$.active_panel_container;
+ this.tabStrip_ = this.$.tab_strip;
+
+ this.dragHandle_ = this.$.side_panel_drag_handle;
+ this.dragHandle_.horizontal = false;
+ this.dragHandle_.target = this.activePanelContainer_;
+ this.rangeOfInterest_ = new tr.b.math.Range();
+ this.brushingStateController_ = undefined;
+ this.onSelectionChanged_ = this.onSelectionChanged_.bind(this);
+ this.onModelChanged_ = this.onModelChanged_.bind(this);
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ set brushingStateController(brushingStateController) {
+ if (this.brushingStateController) {
+ this.brushingStateController_.removeEventListener(
+ 'change', this.onSelectionChanged_);
+ this.brushingStateController_.removeEventListener(
+ 'model-changed', this.onModelChanged_);
+ }
+ this.brushingStateController_ = brushingStateController;
+ if (this.brushingStateController) {
+ this.brushingStateController_.addEventListener(
+ 'change', this.onSelectionChanged_);
+ this.brushingStateController_.addEventListener(
+ 'model-changed', this.onModelChanged_);
+ if (this.model) {
+ this.onModelChanged_();
+ }
+ }
+ },
+
+ onSelectionChanged_() {
+ if (this.activePanel) {
+ this.activePanel.selection = this.selection;
+ }
+ },
+
+ get model() {
+ return this.brushingStateController_.model;
+ },
+
+ onModelChanged_() {
+ this.activePanelType_ = undefined;
+ this.updateContents_();
+ },
+
+ get expanded() {
+ this.hasAttribute('expanded');
+ },
+
+ get activePanel() {
+ return this.activePanelContainer_.children[0];
+ },
+
+ get activePanelType() {
+ return this.activePanelType_;
+ },
+
+ set activePanelType(panelType) {
+ if (this.model === undefined) {
+ throw new Error('Cannot activate panel without a model');
+ }
+
+ let panel = undefined;
+ if (panelType) {
+ panel = document.createElement(panelType);
+ }
+
+ if (panel !== undefined && !panel.supportsModel(this.model)) {
+ throw new Error('Cannot activate panel: does not support this model');
+ }
+
+ if (this.activePanelType) {
+ Polymer.dom(this.getLabelElementForPanelType_(
+ this.activePanelType)).removeAttribute('selected');
+ }
+
+ if (this.activePanelType) {
+ this.getLabelElementForPanelType_(
+ this.activePanelType).removeAttribute('selected');
+ }
+
+ if (this.activePanel) {
+ this.activePanelContainer_.removeChild(this.activePanel);
+ }
+
+ if (panelType === undefined) {
+ Polymer.dom(this).removeAttribute('expanded');
+ this.activePanelType_ = undefined;
+ return;
+ }
+
+ Polymer.dom(this.getLabelElementForPanelType_(panelType)).
+ setAttribute('selected', true);
+ Polymer.dom(this).setAttribute('expanded', true);
+
+ Polymer.dom(this.activePanelContainer_).appendChild(panel);
+ panel.rangeOfInterest = this.rangeOfInterest_;
+ panel.selection = this.selection_;
+ panel.model = this.model;
+
+ this.activePanelType_ = panelType;
+ },
+
+ getPanelTypeForConstructor_(constructor) {
+ for (let i = 0; i < this.tabStrip_.children.length; i++) {
+ if (this.tabStrip_.children[i].panelType.constructor === constructor) {
+ return this.tabStrip_.children[i].panelType;
+ }
+ }
+ },
+
+ getLabelElementForPanelType_(panelType) {
+ for (let i = 0; i < this.tabStrip_.children.length; i++) {
+ if (this.tabStrip_.children[i].panelType === panelType) {
+ return this.tabStrip_.children[i];
+ }
+ }
+ return undefined;
+ },
+
+ updateContents_() {
+ const previouslyActivePanelType = this.activePanelType;
+
+ Polymer.dom(this.tabStrip_).textContent = '';
+ const supportedPanelTypes = [];
+ const panelTypeInfos =
+ tr.ui.side_panel.SidePanelRegistry.getAllRegisteredTypeInfos();
+ const unsupportedLabelEls = [];
+
+ for (const panelTypeInfo of panelTypeInfos) {
+ const labelEl = document.createElement('tab-strip-label');
+ const panel = panelTypeInfo.constructor();
+ const panelType = panel.tagName;
+
+ Polymer.dom(labelEl).textContent = panel.textLabel;
+ labelEl.panelType = panelType;
+
+ const supported = panel.supportsModel(this.model);
+ if (this.model && supported.supported) {
+ supportedPanelTypes.push(panelType);
+ Polymer.dom(labelEl).setAttribute('enabled', true);
+ labelEl.addEventListener('click', function(panelType) {
+ this.activePanelType =
+ this.activePanelType === panelType ? undefined : panelType;
+ }.bind(this, panelType));
+ Polymer.dom(this.tabStrip_).appendChild(labelEl);
+ } else {
+ if (this.activePanel) {
+ this.activePanelContainer_.removeChild(this.activePanel);
+ }
+ this.removeAttribute('expanded');
+ unsupportedLabelEls.push(labelEl);
+ }
+ }
+
+ // Labels do not shrink, so when the user drags the analysis-view up, the
+ // bottom labels are obscured first.
+ // Append all unsupported panel labels after all supported panel labels so
+ // that unsupported panel labels are obscured first.
+ for (const labelEl of unsupportedLabelEls) {
+ Polymer.dom(this.tabStrip_).appendChild(labelEl);
+ }
+
+ // Restore the active panel, or collapse
+ if (previouslyActivePanelType &&
+ supportedPanelTypes.includes(previouslyActivePanelType)) {
+ this.activePanelType = previouslyActivePanelType;
+ Polymer.dom(this).setAttribute('expanded', true);
+ } else {
+ if (this.activePanel) {
+ Polymer.dom(this.activePanelContainer_).removeChild(this.activePanel);
+ }
+ Polymer.dom(this).removeAttribute('expanded');
+ }
+ },
+
+ get rangeOfInterest() {
+ return this.rangeOfInterest_;
+ },
+
+ set rangeOfInterest(range) {
+ if (range === undefined) {
+ throw new Error('Must not be undefined');
+ }
+ this.rangeOfInterest_ = range;
+ if (this.activePanel) {
+ this.activePanel.rangeOfInterest = range;
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html
new file mode 100644
index 00000000000..a51be620426
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_container_test.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/base/deep_utils.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_container.html">
+
+<dom-module id="tr-ui-sp-disabled-side-panel"></dom-module>
+<dom-module id="tr-ui-sp-enabled-side-panel"></dom-module>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function FakeBrushingStateController() {
+ this.addAllEventsMatchingFilterToSelectionReturnValue = [];
+
+ this.viewport = undefined;
+ this.model = undefined;
+ this.selection = new tr.model.EventSet();
+ this.highlight = new tr.model.EventSet();
+ }
+
+ FakeBrushingStateController.prototype = {
+ addEventListener(name, cb) {
+ }
+ };
+
+ function createModel() {
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ const browserProcess = m.getOrCreateProcess(1);
+ const browserMain = browserProcess.getOrCreateThread(2);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 0);
+ browserMain.sliceGroup.endSlice(10);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 20);
+ browserMain.sliceGroup.endSlice(30);
+ }
+ });
+ return m;
+ }
+
+ Polymer({
+ is: 'tr-ui-sp-disabled-test-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+ supportsModel(m) {
+ return {supported: false};
+ },
+ get textLabel() {
+ return 'Disabled';
+ }
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function disabled() {
+ return document.createElement('tr-ui-sp-disabled-test-panel');
+ });
+
+ Polymer({
+ is: 'tr-ui-sp-enabled-test-panel',
+ behaviors: [tr.ui.behaviors.SidePanel],
+ supportsModel(m) {
+ return {supported: true};
+ },
+ get textLabel() {
+ return 'Enabled';
+ },
+ });
+
+ tr.ui.side_panel.SidePanelRegistry.register(function enabled() {
+ return document.createElement('tr-ui-sp-enabled-test-panel');
+ });
+
+ test('instantiateCollapsed', function() {
+ const brushingStateController = new FakeBrushingStateController();
+ brushingStateController.model = createModel();
+
+ const container = document.createElement('tr-ui-side-panel-container');
+ container.brushingStateController = brushingStateController;
+ this.addHTMLOutput(container);
+
+ // The Enabled tab should appear first in the tab strip even though the
+ // disabled side panel was registered first.
+ // There may be other side panels.
+ const labels = tr.ui.b.findDeepElementsMatching(container,
+ 'TAB-STRIP-LABEL').map(e => e.textContent);
+ assert.isBelow(labels.indexOf('Enabled'), labels.indexOf('Disabled'));
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html
new file mode 100644
index 00000000000..0ec139f2225
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+
+<script>
+'use strict';
+
+// TODO(charliea): This can probably be cleaned up so that we don't have to
+// manually wrap the Polymer element names with a function and
+// `document.createElement` at each of the registration sites by creating a
+// new "Polymer" registration mode.
+tr.exportTo('tr.ui.side_panel', function() {
+ /**
+ * SidePanelRegistry is an entity for side panel Polymer elements to register
+ * on so that they'll render a side panel if the model has the correct data.
+ *
+ * Example usage:
+ *
+ * SidePanelRegistry.register(function() {
+ * return document.createElement('my-side-panel');
+ * });
+ *
+ * @constructor
+ */
+ function SidePanelRegistry() {}
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(SidePanelRegistry, options);
+
+ return {
+ SidePanelRegistry,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html
new file mode 100644
index 00000000000..c174d5eb005
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/side_panel/side_panel_registry_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SidePanelRegistry = tr.ui.side_panel.SidePanelRegistry;
+
+ const testOptions = {
+ setUp() {
+ SidePanelRegistry.pushCleanStateBeforeTest();
+ },
+
+ tearDown() {
+ SidePanelRegistry.popCleanStateAfterTest();
+ },
+ };
+
+ test('register', function() {
+ SidePanelRegistry.register(function() {
+ return document.createElement('div');
+ });
+ SidePanelRegistry.register(function() {
+ return document.createElement('span');
+ });
+
+ const typeInfos = SidePanelRegistry.getAllRegisteredTypeInfos();
+ assert.strictEqual(typeInfos[0].constructor().tagName, 'DIV');
+ assert.strictEqual(typeInfos[1].constructor().tagName, 'SPAN');
+ assert.lengthOf(typeInfos, 2);
+ }, testOptions);
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html
new file mode 100644
index 00000000000..2aefa6d6de4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ function TimelineDisplayTransform(opt_that) {
+ if (opt_that) {
+ this.set(opt_that);
+ return;
+ }
+ this.scaleX = 1;
+ this.panX = 0;
+ this.panY = 0;
+ }
+
+ TimelineDisplayTransform.prototype = {
+ set(that) {
+ this.scaleX = that.scaleX;
+ this.panX = that.panX;
+ this.panY = that.panY;
+ },
+
+ clone() {
+ return new TimelineDisplayTransform(this);
+ },
+
+ equals(that) {
+ let eq = true;
+ if (that === undefined || that === null) {
+ return false;
+ }
+ eq &= this.panX === that.panX;
+ eq &= this.panY === that.panY;
+ eq &= this.scaleX === that.scaleX;
+ return !!eq;
+ },
+
+ almostEquals(that) {
+ let eq = true;
+ if (that === undefined || that === null) {
+ return false;
+ }
+ eq &= Math.abs(this.panX - that.panX) < 0.001;
+ eq &= Math.abs(this.panY - that.panY) < 0.001;
+ eq &= Math.abs(this.scaleX - that.scaleX) < 0.001;
+ return !!eq;
+ },
+
+ incrementPanXInViewUnits(xDeltaView) {
+ this.panX += this.xViewVectorToWorld(xDeltaView);
+ },
+
+ xPanWorldPosToViewPos(worldX, viewX, viewWidth) {
+ if (typeof viewX === 'string') {
+ if (viewX === 'left') {
+ viewX = 0;
+ } else if (viewX === 'center') {
+ viewX = viewWidth / 2;
+ } else if (viewX === 'right') {
+ viewX = viewWidth - 1;
+ } else {
+ throw new Error('viewX must be left|center|right or number.');
+ }
+ }
+ this.panX = (viewX / this.scaleX) - worldX;
+ },
+
+ xPanWorldBoundsIntoView(worldMin, worldMax, viewWidth) {
+ if (this.xWorldToView(worldMin) < 0) {
+ this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth);
+ } else if (this.xWorldToView(worldMax) > viewWidth) {
+ this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth);
+ }
+ },
+
+ xSetWorldBounds(worldMin, worldMax, viewWidth) {
+ const worldWidth = worldMax - worldMin;
+ const scaleX = viewWidth / worldWidth;
+ const panX = -worldMin;
+ this.setPanAndScale(panX, scaleX);
+ },
+
+ setPanAndScale(p, s) {
+ this.scaleX = s;
+ this.panX = p;
+ },
+
+ xWorldToView(x) {
+ return (x + this.panX) * this.scaleX;
+ },
+
+ xWorldVectorToView(x) {
+ return x * this.scaleX;
+ },
+
+ xViewToWorld(x) {
+ return (x / this.scaleX) - this.panX;
+ },
+
+ xViewVectorToWorld(x) {
+ return x / this.scaleX;
+ }
+ };
+
+ return {
+ TimelineDisplayTransform,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html
new file mode 100644
index 00000000000..a632a6dc4fd
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations.html
@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/animation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ const kDefaultPanAnimationDurationMs = 100.0;
+ const lerp = tr.b.math.lerp;
+
+ /**
+ * Pans a TimelineDisplayTransform by a given amount.
+ * @constructor
+ * @extends {tr.ui.b.Animation}
+ * @param {Number} deltaX The total amount of change to the transform's panX.
+ * @param {Number} deltaY The total amount of change to the transform's panY.
+ * @param {Number=} opt_durationMs How long the pan animation should run.
+ * Defaults to kDefaultPanAnimationDurationMs.
+ */
+ function TimelineDisplayTransformPanAnimation(
+ deltaX, deltaY, opt_durationMs) {
+ this.deltaX = deltaX;
+ this.deltaY = deltaY;
+ if (opt_durationMs === undefined) {
+ this.durationMs = kDefaultPanAnimationDurationMs;
+ } else {
+ this.durationMs = opt_durationMs;
+ }
+
+ this.startPanX = undefined;
+ this.startPanY = undefined;
+ this.startTimeMs = undefined;
+ }
+
+ TimelineDisplayTransformPanAnimation.prototype = {
+ __proto__: tr.ui.b.Animation.prototype,
+
+ get affectsPanY() {
+ return this.deltaY !== 0;
+ },
+
+ canTakeOverFor(existingAnimation) {
+ return existingAnimation instanceof TimelineDisplayTransformPanAnimation;
+ },
+
+ takeOverFor(existing, timestamp, target) {
+ const remainingDeltaXOnExisting = existing.goalPanX - target.panX;
+ const remainingDeltaYOnExisting = existing.goalPanY - target.panY;
+ let remainingTimeOnExisting = timestamp - (
+ existing.startTimeMs + existing.durationMs);
+ remainingTimeOnExisting = Math.max(remainingTimeOnExisting, 0);
+
+ this.deltaX += remainingDeltaXOnExisting;
+ this.deltaY += remainingDeltaYOnExisting;
+ this.durationMs += remainingTimeOnExisting;
+ },
+
+ start(timestamp, target) {
+ this.startTimeMs = timestamp;
+ this.startPanX = target.panX;
+ this.startPanY = target.panY;
+ },
+
+ tick(timestamp, target) {
+ let percentDone = (timestamp - this.startTimeMs) / this.durationMs;
+ percentDone = tr.b.math.clamp(percentDone, 0, 1);
+
+ target.panX = lerp(percentDone, this.startPanX, this.goalPanX);
+ if (this.affectsPanY) {
+ target.panY = lerp(percentDone, this.startPanY, this.goalPanY);
+ }
+ return timestamp >= this.startTimeMs + this.durationMs;
+ },
+
+ get goalPanX() {
+ return this.startPanX + this.deltaX;
+ },
+
+ get goalPanY() {
+ return this.startPanY + this.deltaY;
+ }
+ };
+
+ /**
+ * Zooms in/out on a specified location in the world.
+ *
+ * Zooming in and out is all about keeping the area under the mouse cursor,
+ * here called the "focal point" in the same place under the zoom. If one
+ * simply changes the scale, the area under the mouse cursor will change. To
+ * keep the focal point from moving during the zoom, the pan needs to change
+ * in order to compensate. Thus, a ZoomTo animation is given both a focal
+ * point in addition to the amount by which to zoom.
+ *
+ * @constructor
+ * @extends {tr.ui.b.Animation}
+ * @param {Number} goalFocalPointXWorld The X coordinate in the world which is
+ * of interest.
+ * @param {Number} goalFocalPointXView Where on the screen the
+ * goalFocalPointXWorld should stay centered during the zoom.
+ * @param {Number} goalFocalPointY Where the panY should be when the zoom
+ * completes.
+ * @param {Number} zoomInRatioX The ratio of the current scaleX to the goal
+ * scaleX.
+ */
+ function TimelineDisplayTransformZoomToAnimation(
+ goalFocalPointXWorld,
+ goalFocalPointXView,
+ goalFocalPointY,
+ zoomInRatioX,
+ opt_durationMs) {
+ this.goalFocalPointXWorld = goalFocalPointXWorld;
+ this.goalFocalPointXView = goalFocalPointXView;
+ this.goalFocalPointY = goalFocalPointY;
+ this.zoomInRatioX = zoomInRatioX;
+ if (opt_durationMs === undefined) {
+ this.durationMs = kDefaultPanAnimationDurationMs;
+ } else {
+ this.durationMs = opt_durationMs;
+ }
+
+ this.startTimeMs = undefined;
+ this.startScaleX = undefined;
+ this.goalScaleX = undefined;
+ this.startPanY = undefined;
+ }
+
+ TimelineDisplayTransformZoomToAnimation.prototype = {
+ __proto__: tr.ui.b.Animation.prototype,
+
+ get affectsPanY() {
+ return this.startPanY !== this.goalFocalPointY;
+ },
+
+ canTakeOverFor(existingAnimation) {
+ return false;
+ },
+
+ takeOverFor(existingAnimation, timestamp, target) {
+ this.goalScaleX = target.scaleX * this.zoomInRatioX;
+ },
+
+ start(timestamp, target) {
+ this.startTimeMs = timestamp;
+ this.startScaleX = target.scaleX;
+ this.goalScaleX = this.zoomInRatioX * target.scaleX;
+ this.startPanY = target.panY;
+ },
+
+ tick(timestamp, target) {
+ let percentDone = (timestamp - this.startTimeMs) / this.durationMs;
+ percentDone = tr.b.math.clamp(percentDone, 0, 1);
+
+ target.scaleX = lerp(percentDone, this.startScaleX, this.goalScaleX);
+ if (this.affectsPanY) {
+ target.panY = lerp(percentDone, this.startPanY, this.goalFocalPointY);
+ }
+
+ target.xPanWorldPosToViewPos(
+ this.goalFocalPointXWorld, this.goalFocalPointXView);
+ return timestamp >= this.startTimeMs + this.durationMs;
+ }
+ };
+
+ return {
+ TimelineDisplayTransformPanAnimation,
+ TimelineDisplayTransformZoomToAnimation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html
new file mode 100644
index 00000000000..215a8863885
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_animations_test.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/animation_controller.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform_animations.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const TimelineDisplayTransformPanAnimation =
+ tr.ui.TimelineDisplayTransformPanAnimation;
+ const TimelineDisplayTransformZoomToAnimation =
+ tr.ui.TimelineDisplayTransformZoomToAnimation;
+
+ test('panBasic', function() {
+ const target = new TimelineDisplayTransform();
+ target.cloneAnimationState = function() {
+ return this.clone();
+ };
+
+ const a = new TimelineDisplayTransformPanAnimation(10, 20, 100);
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+ controller.queueAnimation(a, 0);
+
+ assert.isTrue(a.affectsPanY);
+ tr.b.forcePendingRAFTasksToRun(50);
+ assert.isAbove(target.panX, 0);
+ tr.b.forcePendingRAFTasksToRun(100);
+ assert.isFalse(controller.hasActiveAnimation);
+ assert.strictEqual(target.panX, 10);
+ assert.strictEqual(target.panY, 20);
+ });
+
+ test('zoomBasic', function() {
+ const target = new TimelineDisplayTransform();
+ target.panY = 30;
+ target.cloneAnimationState = function() {
+ return this.clone();
+ };
+
+ const a = new TimelineDisplayTransformZoomToAnimation(10, 20, 30, 5, 100);
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+ controller.queueAnimation(a, 0);
+
+ assert.isFalse(a.affectsPanY);
+ tr.b.forcePendingRAFTasksToRun(100);
+ assert.strictEqual(target.scaleX, 5);
+ });
+
+ test('panTakeover', function() {
+ const target = new TimelineDisplayTransform();
+ target.cloneAnimationState = function() {
+ return this.clone();
+ };
+
+ const b = new TimelineDisplayTransformPanAnimation(10, 0, 100);
+ const a = new TimelineDisplayTransformPanAnimation(10, 0, 100);
+
+ const controller = new tr.ui.b.AnimationController();
+ controller.target = target;
+ controller.queueAnimation(a, 0);
+
+ tr.b.forcePendingRAFTasksToRun(50);
+ controller.queueAnimation(b, 50);
+
+ tr.b.forcePendingRAFTasksToRun(100);
+ assert.isTrue(controller.hasActiveAnimation);
+
+ tr.b.forcePendingRAFTasksToRun(150);
+ assert.isFalse(controller.hasActiveAnimation);
+ assert.strictEqual(target.panX, 20);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html
new file mode 100644
index 00000000000..d0df289c34e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_display_transform_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+
+ test('basics', function() {
+ const a = new TimelineDisplayTransform();
+ a.panX = 0;
+ a.panY = 0;
+ a.scaleX = 1;
+
+ const b = new TimelineDisplayTransform();
+ b.panX = 10;
+ b.panY = 0;
+ b.scaleX = 1;
+
+ assert.isFalse(a.equals(b));
+ assert.isFalse(a.almostEquals(b));
+
+ const c = b.clone();
+ assert.isTrue(b.equals(c));
+ assert.isTrue(b.almostEquals(c));
+
+ c.set(a);
+ assert.isTrue(a.equals(c));
+ assert.isTrue(a.almostEquals(c));
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html
new file mode 100644
index 00000000000..36126f898db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_interest_range.html
@@ -0,0 +1,249 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui', function() {
+ /**
+ * @constructor
+ */
+ function SnapIndicator(y, height) {
+ this.y = y;
+ this.height = height;
+ }
+
+ /**
+ * The interesting part of the world.
+ *
+ * @constructor
+ */
+ function TimelineInterestRange(vp) {
+ this.viewport_ = vp;
+
+ this.range_ = new tr.b.math.Range();
+
+ this.leftSelected_ = false;
+ this.rightSelected_ = false;
+
+ this.leftSnapIndicator_ = undefined;
+ this.rightSnapIndicator_ = undefined;
+ }
+
+ TimelineInterestRange.prototype = {
+ get isEmpty() {
+ return this.range_.isEmpty;
+ },
+
+ reset() {
+ this.range_.reset();
+ this.leftSelected_ = false;
+ this.rightSelected_ = false;
+ this.leftSnapIndicator_ = undefined;
+ this.rightSnapIndicator_ = undefined;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get min() {
+ return this.range_.min;
+ },
+
+ set min(min) {
+ this.range_.min = min;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get max() {
+ return this.range_.max;
+ },
+
+ set max(max) {
+ this.range_.max = max;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ set(range) {
+ this.range_.reset();
+ this.range_.addRange(range);
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ setMinAndMax(min, max) {
+ this.range_.min = min;
+ this.range_.max = max;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get range() {
+ return this.range_.range;
+ },
+
+ asRangeObject() {
+ const range = new tr.b.math.Range();
+ range.addRange(this.range_);
+ return range;
+ },
+
+ get leftSelected() {
+ return this.leftSelected_;
+ },
+
+ set leftSelected(leftSelected) {
+ if (this.leftSelected_ === leftSelected) return;
+
+ this.leftSelected_ = leftSelected;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get rightSelected() {
+ return this.rightSelected_;
+ },
+
+ set rightSelected(rightSelected) {
+ if (this.rightSelected_ === rightSelected) return;
+
+ this.rightSelected_ = rightSelected;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get leftSnapIndicator() {
+ return this.leftSnapIndicator_;
+ },
+
+ set leftSnapIndicator(leftSnapIndicator) {
+ this.leftSnapIndicator_ = leftSnapIndicator;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ get rightSnapIndicator() {
+ return this.rightSnapIndicator_;
+ },
+
+ set rightSnapIndicator(rightSnapIndicator) {
+ this.rightSnapIndicator_ = rightSnapIndicator;
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ draw(ctx, viewLWorld, viewRWorld, viewHeight) {
+ if (this.range_.isEmpty) return;
+
+ const dt = this.viewport_.currentDisplayTransform;
+
+ const markerLWorld = this.min;
+ const markerRWorld = this.max;
+
+ const markerLView = Math.round(dt.xWorldToView(markerLWorld));
+ const markerRView = Math.round(dt.xWorldToView(markerRWorld));
+
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
+ if (markerLWorld > viewLWorld) {
+ ctx.fillRect(dt.xWorldToView(viewLWorld), 0,
+ markerLView, viewHeight);
+ }
+
+ if (markerRWorld < viewRWorld) {
+ ctx.fillRect(markerRView, 0,
+ dt.xWorldToView(viewRWorld), viewHeight);
+ }
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ ctx.lineWidth = Math.round(pixelRatio);
+ if (this.range_.range > 0) {
+ this.drawLine_(ctx, viewLWorld, viewRWorld,
+ viewHeight, this.min, this.leftSelected_);
+ this.drawLine_(ctx, viewLWorld, viewRWorld,
+ viewHeight, this.max, this.rightSelected_);
+ } else {
+ this.drawLine_(ctx, viewLWorld, viewRWorld,
+ viewHeight, this.min,
+ this.leftSelected_ || this.rightSelected_);
+ }
+ ctx.lineWidth = 1;
+ },
+
+ drawLine_(ctx, viewLWorld, viewRWorld, height, ts, selected) {
+ if (ts < viewLWorld || ts >= viewRWorld) return;
+
+ const dt = this.viewport_.currentDisplayTransform;
+ const viewX = Math.round(dt.xWorldToView(ts));
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ ctx.save();
+ ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+ ctx.beginPath();
+ tr.ui.b.drawLine(ctx, viewX, 0, viewX, height);
+ if (selected) {
+ ctx.strokeStyle = 'rgb(255, 0, 0)';
+ } else {
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ }
+ ctx.stroke();
+
+ ctx.restore();
+ },
+
+ drawIndicators(ctx, viewLWorld, viewRWorld) {
+ if (this.leftSnapIndicator_) {
+ this.drawIndicator_(ctx, viewLWorld, viewRWorld,
+ this.range_.min,
+ this.leftSnapIndicator_,
+ this.leftSelected_);
+ }
+ if (this.rightSnapIndicator_) {
+ this.drawIndicator_(ctx, viewLWorld, viewRWorld,
+ this.range_.max,
+ this.rightSnapIndicator_,
+ this.rightSelected_);
+ }
+ },
+
+ drawIndicator_(ctx, viewLWorld, viewRWorld,
+ xWorld, si, selected) {
+ const dt = this.viewport_.currentDisplayTransform;
+
+ const viewX = Math.round(dt.xWorldToView(xWorld));
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ ctx.save();
+ ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const viewY = si.y * devicePixelRatio;
+ const viewHeight = si.height * devicePixelRatio;
+ const arrowSize = 4 * pixelRatio;
+
+ if (selected) {
+ ctx.fillStyle = 'rgb(255, 0, 0)';
+ } else {
+ ctx.fillStyle = 'rgb(0, 0, 0)';
+ }
+ tr.ui.b.drawTriangle(ctx,
+ viewX - arrowSize * 0.75, viewY,
+ viewX + arrowSize * 0.75, viewY,
+ viewX, viewY + arrowSize);
+ ctx.fill();
+ tr.ui.b.drawTriangle(ctx,
+ viewX - arrowSize * 0.75, viewY + viewHeight,
+ viewX + arrowSize * 0.75, viewY + viewHeight,
+ viewX, viewY + viewHeight - arrowSize);
+ ctx.fill();
+
+ ctx.restore();
+ }
+ };
+
+ return {
+ SnapIndicator,
+ TimelineInterestRange,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html
new file mode 100644
index 00000000000..f6087abf74c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view.html
@@ -0,0 +1,1179 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/x_marker_annotation.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html">
+<link rel="import" href="/tracing/ui/base/timing_tool.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform_animations.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/model_track.html">
+<link rel="import" href="/tracing/ui/tracks/x_axis_track.html">
+
+<!--
+ Interactive visualizaiton of Model objects based loosely on gantt charts.
+ Each thread in the Model is given a set of Tracks, one per subrow in the
+ thread. The TimelineTrackView class acts as a controller, creating the
+ individual tracks, while Tracks do actual drawing.
+
+ Visually, the TimelineTrackView produces (prettier) visualizations like the
+ following:
+ Thread1: AAAAAAAAAA AAAAA
+ BBBB BB
+ Thread2: CCCCCC CCCCC
+-->
+<dom-module id='tr-ui-timeline-track-view'>
+ <template>
+ <style>
+ :host {
+ flex-direction: column;
+ display: flex;
+ position: relative;
+ }
+
+ :host ::content * {
+ -webkit-user-select: none;
+ cursor: default;
+ }
+
+ #drag_box {
+ background-color: rgba(0, 0, 255, 0.25);
+ border: 1px solid rgb(0, 0, 96);
+ font-size: 75%;
+ position: fixed;
+ }
+
+ #hint_text {
+ position: absolute;
+ bottom: 6px;
+ right: 6px;
+ font-size: 8pt;
+ }
+ </style>
+ <slot></slot>
+
+ <div id='drag_box'></div>
+ <div id='hint_text'></div>
+
+ <tv-ui-b-hotkey-controller id='hotkey_controller'>
+ </tv-ui-b-hotkey-controller>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-timeline-track-view',
+
+ ready() {
+ this.displayTransform_ = new tr.ui.TimelineDisplayTransform();
+ this.model_ = undefined;
+
+ this.timelineView_ = undefined;
+ this.pollIfViewportAttachedInterval_ = undefined;
+
+ this.viewport_ = new tr.ui.TimelineViewport(this);
+ this.viewportDisplayTransformAtMouseDown_ = undefined;
+ this.brushingStateController_ = undefined;
+
+ this.rulerTrackContainer_ =
+ new tr.ui.tracks.DrawingContainer(this.viewport_);
+ Polymer.dom(this).appendChild(this.rulerTrackContainer_);
+ this.rulerTrackContainer_.invalidate();
+ this.rulerTrackContainer_.style.overflowY = 'hidden';
+ this.rulerTrackContainer_.style.flexShrink = '0';
+
+ this.rulerTrack_ = new tr.ui.tracks.XAxisTrack(this.viewport_);
+ Polymer.dom(this.rulerTrackContainer_).appendChild(this.rulerTrack_);
+
+ this.upperModelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_);
+ this.upperModelTrack_.upperMode = true;
+ Polymer.dom(this.rulerTrackContainer_).appendChild(this.upperModelTrack_);
+
+ this.modelTrackContainer_ =
+ new tr.ui.tracks.DrawingContainer(this.viewport_);
+ Polymer.dom(this).appendChild(this.modelTrackContainer_);
+ this.modelTrackContainer_.style.display = 'block';
+ this.modelTrackContainer_.style.flexGrow = '1';
+ this.modelTrackContainer_.invalidate();
+
+ this.viewport_.modelTrackContainer = this.modelTrackContainer_;
+
+ this.modelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_);
+ Polymer.dom(this.modelTrackContainer_).appendChild(this.modelTrack_);
+
+ this.timingTool_ = new tr.ui.b.TimingTool(this.viewport_, this);
+
+ this.initMouseModeSelector();
+
+ this.hideDragBox_();
+
+ this.initHintText_();
+
+ this.onSelectionChanged_ = this.onSelectionChanged_.bind(this);
+
+ this.onDblClick_ = this.onDblClick_.bind(this);
+ this.addEventListener('dblclick', this.onDblClick_);
+
+ this.onMouseWheel_ = this.onMouseWheel_.bind(this);
+ this.addEventListener('mousewheel', this.onMouseWheel_);
+
+ this.onMouseDown_ = this.onMouseDown_.bind(this);
+ this.addEventListener('mousedown', this.onMouseDown_);
+
+ this.onMouseMove_ = this.onMouseMove_.bind(this);
+ this.addEventListener('mousemove', this.onMouseMove_);
+
+ this.onTouchStart_ = this.onTouchStart_.bind(this);
+ this.addEventListener('touchstart', this.onTouchStart_);
+
+ this.onTouchMove_ = this.onTouchMove_.bind(this);
+ this.addEventListener('touchmove', this.onTouchMove_);
+
+ this.onTouchEnd_ = this.onTouchEnd_.bind(this);
+ this.addEventListener('touchend', this.onTouchEnd_);
+
+
+ this.addHotKeys_();
+
+ this.mouseViewPosAtMouseDown_ = {x: 0, y: 0};
+ this.lastMouseViewPos_ = {x: 0, y: 0};
+
+ this.lastTouchViewPositions_ = [];
+
+ this.alert_ = undefined;
+
+ this.isPanningAndScanning_ = false;
+ this.isZooming_ = false;
+ },
+
+ initMouseModeSelector() {
+ this.mouseModeSelector_ = document.createElement(
+ 'tr-ui-b-mouse-mode-selector');
+ this.mouseModeSelector_.targetElement = this;
+ Polymer.dom(this).appendChild(this.mouseModeSelector_);
+
+ this.mouseModeSelector_.addEventListener('beginpan',
+ this.onBeginPanScan_.bind(this));
+ this.mouseModeSelector_.addEventListener('updatepan',
+ this.onUpdatePanScan_.bind(this));
+ this.mouseModeSelector_.addEventListener('endpan',
+ this.onEndPanScan_.bind(this));
+
+ this.mouseModeSelector_.addEventListener('beginselection',
+ this.onBeginSelection_.bind(this));
+ this.mouseModeSelector_.addEventListener('updateselection',
+ this.onUpdateSelection_.bind(this));
+ this.mouseModeSelector_.addEventListener('endselection',
+ this.onEndSelection_.bind(this));
+
+ this.mouseModeSelector_.addEventListener('beginzoom',
+ this.onBeginZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('updatezoom',
+ this.onUpdateZoom_.bind(this));
+ this.mouseModeSelector_.addEventListener('endzoom',
+ this.onEndZoom_.bind(this));
+
+ this.mouseModeSelector_.addEventListener('entertiming',
+ this.timingTool_.onEnterTiming.bind(this.timingTool_));
+ this.mouseModeSelector_.addEventListener('begintiming',
+ this.timingTool_.onBeginTiming.bind(this.timingTool_));
+ this.mouseModeSelector_.addEventListener('updatetiming',
+ this.timingTool_.onUpdateTiming.bind(this.timingTool_));
+ this.mouseModeSelector_.addEventListener('endtiming',
+ this.timingTool_.onEndTiming.bind(this.timingTool_));
+ this.mouseModeSelector_.addEventListener('exittiming',
+ this.timingTool_.onExitTiming.bind(this.timingTool_));
+
+ const m = tr.ui.b.MOUSE_SELECTOR_MODE;
+ this.mouseModeSelector_.supportedModeMask =
+ m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING;
+ this.mouseModeSelector_.settingsKey =
+ 'timelineTrackView.mouseModeSelector';
+ this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0));
+ this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0));
+ this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0));
+ this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0));
+
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ m.SELECTION, tr.ui.b.MODIFIER.SHIFT);
+ this.mouseModeSelector_.setModifierForAlternateMode(
+ m.PANSCAN, tr.ui.b.MODIFIER.SPACE);
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ set brushingStateController(brushingStateController) {
+ if (this.brushingStateController_) {
+ this.brushingStateController_.removeEventListener('change',
+ this.onSelectionChanged_);
+ }
+ this.brushingStateController_ = brushingStateController;
+ if (this.brushingStateController_) {
+ this.brushingStateController_.addEventListener('change',
+ this.onSelectionChanged_);
+ }
+ },
+
+ set timelineView(view) {
+ this.timelineView_ = view;
+ },
+
+ get processViews() {
+ return this.modelTrack_.processViews;
+ },
+
+ onSelectionChanged_() {
+ this.showHintText_('Press \'m\' to mark current selection');
+ this.viewport_.dispatchChangeEvent();
+ },
+
+ set selection(selection) {
+ throw new Error('DO NOT CALL THIS');
+ },
+
+ set highlight(highlight) {
+ throw new Error('DO NOT CALL THIS');
+ },
+
+ detach() {
+ this.modelTrack_.detach();
+ this.upperModelTrack_.detach();
+
+ if (this.pollIfViewportAttachedInterval_) {
+ window.clearInterval(this.pollIfViewportAttachedInterval_);
+ this.pollIfViewportAttachedInterval_ = undefined;
+ }
+ this.viewport_.detach();
+ },
+
+ get viewport() {
+ return this.viewport_;
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ if (!model) {
+ throw new Error('Model cannot be undefined');
+ }
+
+ const modelInstanceChanged = this.model_ !== model;
+ this.model_ = model;
+ this.modelTrack_.model = model;
+ this.upperModelTrack_.model = model;
+
+ // Set up a reasonable viewport.
+ if (modelInstanceChanged) {
+ // The following code uses an interval to detect when the parent element
+ // is attached to the document. That is a trigger to run the setup
+ // function and install a resize listener.
+ this.pollIfViewportAttachedInterval_ = window.setInterval(
+ this.pollIfViewportAttached_.bind(this), 250);
+ }
+ },
+
+ get hasVisibleContent() {
+ return this.modelTrack_.hasVisibleContent ||
+ this.upperModelTrack_.hasVisibleContent;
+ },
+
+ /**
+ * Checks whether the parentNode is attached to the document.
+ * When it is, the method installs the iframe-based resize detection hook
+ * and then runs setInitialViewport_, if present.
+ */
+ pollIfViewportAttached_() {
+ if (!this.viewport_.isAttachedToDocumentOrInTestMode ||
+ this.viewport_.clientWidth === 0) {
+ return;
+ }
+ window.addEventListener(
+ 'resize', this.viewport_.dispatchChangeEvent);
+ window.clearInterval(this.pollIfViewportAttachedInterval_);
+ this.pollIfViewportAttachedInterval_ = undefined;
+
+ this.setInitialViewport_();
+ },
+
+ setInitialViewport_() {
+ // We need the canvas size to be up-to-date at this point. We maybe in
+ // here before the raf fires, so the size may have not been updated since
+ // the canvas was resized.
+ this.modelTrackContainer_.updateCanvasSizeIfNeeded_();
+ const w = this.modelTrackContainer_.canvas.width;
+
+ let min;
+ let range;
+
+ if (this.model_.bounds.isEmpty) {
+ min = 0;
+ range = 1000;
+ } else if (this.model_.bounds.range === 0) {
+ min = this.model_.bounds.min;
+ range = 1000;
+ } else {
+ min = this.model_.bounds.min;
+ range = this.model_.bounds.range;
+ }
+
+ const boost = range * 0.15;
+ this.displayTransform_.set(this.viewport_.currentDisplayTransform);
+ this.displayTransform_.xSetWorldBounds(
+ min - boost, min + range + boost, w);
+ this.viewport_.setDisplayTransformImmediately(this.displayTransform_);
+ },
+
+ /**
+ * @param {Filter} filter The filter to use for finding matches.
+ * @param {Selection} selection The selection to add matches to.
+ * @return {Task} which performs the filtering.
+ */
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ const modelTrack = this.modelTrack_;
+ const firstT = modelTrack.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, selection);
+ const lastT = firstT.after(function() {
+ this.upperModelTrack_.addAllEventsMatchingFilterToSelection(
+ filter, selection);
+ }, this);
+ return firstT;
+ },
+
+ onMouseMove_(e) {
+ // Zooming requires the delta since the last mousemove so we need to avoid
+ // tracking it when the zoom interaction is active.
+ if (this.isZooming_) return;
+
+ this.storeLastMousePos_(e);
+ },
+
+ onTouchStart_(e) {
+ this.storeLastTouchPositions_(e);
+ this.focusElements_();
+ },
+
+ onTouchMove_(e) {
+ e.preventDefault();
+ this.onUpdateTransformForTouch_(e);
+ },
+
+ onTouchEnd_(e) {
+ this.storeLastTouchPositions_(e);
+ this.focusElements_();
+ },
+
+ addHotKeys_() {
+ this.addKeyDownHotKeys_();
+ this.addKeyPressHotKeys_();
+ },
+
+ addKeyPressHotKey(dict) {
+ dict.eventType = 'keypress';
+ dict.useCapture = false;
+ dict.thisArg = this;
+ const binding = new tr.ui.b.HotKey(dict);
+ this.$.hotkey_controller.addHotKey(binding);
+ },
+
+ addKeyPressHotKeys_() {
+ this.addKeyPressHotKey({
+ keyCodes: ['w'.charCodeAt(0), ','.charCodeAt(0)],
+ callback(e) {
+ this.zoomBy_(1.5, true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCodes: ['s'.charCodeAt(0), 'o'.charCodeAt(0)],
+ callback(e) {
+ this.zoomBy_(1 / 1.5, true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'g'.charCodeAt(0),
+ callback(e) {
+ this.onGridToggle_(true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'G'.charCodeAt(0),
+ callback(e) {
+ this.onGridToggle_(false);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCodes: ['W'.charCodeAt(0), '<'.charCodeAt(0)],
+ callback(e) {
+ this.zoomBy_(10, true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCodes: ['S'.charCodeAt(0), 'O'.charCodeAt(0)],
+ callback(e) {
+ this.zoomBy_(1 / 10, true);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'a'.charCodeAt(0),
+ callback(e) {
+ this.queueSmoothPan_(this.viewWidth_ * 0.3, 0);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCodes: ['d'.charCodeAt(0), 'e'.charCodeAt(0)],
+ callback(e) {
+ this.queueSmoothPan_(this.viewWidth_ * -0.3, 0);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'A'.charCodeAt(0),
+ callback(e) {
+ this.queueSmoothPan_(viewWidth * 0.5, 0);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'D'.charCodeAt(0),
+ callback(e) {
+ this.queueSmoothPan_(viewWidth * -0.5, 0);
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: '0'.charCodeAt(0),
+ callback(e) {
+ this.setInitialViewport_();
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'f'.charCodeAt(0),
+ callback(e) {
+ this.zoomToSelection();
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'm'.charCodeAt(0),
+ callback(e) {
+ this.setCurrentSelectionAsInterestRange_();
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'p'.charCodeAt(0),
+ callback(e) {
+ this.selectPowerSamplesInCurrentTimeRange_();
+ e.stopPropagation();
+ }
+ });
+
+ this.addKeyPressHotKey({
+ keyCode: 'h'.charCodeAt(0),
+ callback(e) {
+ this.toggleHighDetails_();
+ e.stopPropagation();
+ }
+ });
+ },
+
+ get viewWidth_() {
+ return this.modelTrackContainer_.canvas.clientWidth;
+ },
+
+ addKeyDownHotKeys_() {
+ const addBinding = function(dict) {
+ dict.eventType = 'keydown';
+ dict.useCapture = false;
+ dict.thisArg = this;
+ const binding = new tr.ui.b.HotKey(dict);
+ this.$.hotkey_controller.addHotKey(binding);
+ }.bind(this);
+
+ addBinding({
+ keyCode: 37, // Left arrow.
+ callback(e) {
+ const curSel = this.brushingStateController_.selection;
+ const sel = this.viewport.getShiftedSelection(curSel, -1);
+
+ if (sel) {
+ this.brushingStateController.changeSelectionFromTimeline(sel);
+ this.panToSelection();
+ } else {
+ this.queueSmoothPan_(this.viewWidth_ * 0.3, 0);
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ });
+
+ addBinding({
+ keyCode: 39, // Right arrow.
+ callback(e) {
+ const curSel = this.brushingStateController_.selection;
+ const sel = this.viewport.getShiftedSelection(curSel, 1);
+ if (sel) {
+ this.brushingStateController.changeSelectionFromTimeline(sel);
+ this.panToSelection();
+ } else {
+ this.queueSmoothPan_(-this.viewWidth_ * 0.3, 0);
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ });
+ },
+
+ onDblClick_(e) {
+ if (this.mouseModeSelector_.mode !==
+ tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) {
+ return;
+ }
+
+ const curSelection = this.brushingStateController_.selection;
+ if (!curSelection.length || !tr.b.getOnlyElement(curSelection).title) {
+ return;
+ }
+
+ const selection = new tr.model.EventSet();
+ const filter = new tr.c.ExactTitleFilter(
+ tr.b.getOnlyElement(curSelection).title);
+ this.modelTrack_.addAllEventsMatchingFilterToSelection(filter,
+ selection);
+
+ this.brushingStateController.changeSelectionFromTimeline(selection);
+ },
+
+ onMouseWheel_(e) {
+ if (!e.altKey) return;
+
+ const delta = e.wheelDelta / 120;
+ const zoomScale = Math.pow(1.5, delta);
+ this.zoomBy_(zoomScale);
+ e.preventDefault();
+ },
+
+ onMouseDown_(e) {
+ if (this.mouseModeSelector_.mode !==
+ tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) {
+ return;
+ }
+
+ // Mouse down must start on ruler track for crosshair guide lines to draw.
+ if (e.target !== this.rulerTrack_) return;
+
+ // Make sure we don't start a selection drag event here.
+ this.dragBeginEvent_ = undefined;
+
+ // Remove nav string marker if it exists, since we're clearing the
+ // find control box.
+ if (this.xNavStringMarker_) {
+ this.model.removeAnnotation(this.xNavStringMarker_);
+ this.xNavStringMarker_ = undefined;
+ }
+
+ const dt = this.viewport_.currentDisplayTransform;
+ tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { // Mouse move handler.
+ // If mouse event is on ruler, don't do anything.
+ if (e.target === this.rulerTrack_) return;
+
+ const relativePosition = this.extractRelativeMousePosition_(e);
+ const loc = tr.model.Location.fromViewCoordinates(
+ this.viewport_, relativePosition.x, relativePosition.y);
+ // Not all points on the timeline represents a valid location.
+ // ex. process header tracks, letter dot tracks.
+ if (!loc) return;
+
+ if (this.guideLineAnnotation_ === undefined) {
+ this.guideLineAnnotation_ =
+ new tr.model.XMarkerAnnotation(loc.xWorld);
+ this.model.addAnnotation(this.guideLineAnnotation_);
+ } else {
+ this.guideLineAnnotation_.timestamp = loc.xWorld;
+ this.modelTrackContainer_.invalidate();
+ }
+
+ // Set the findcontrol's text to nav string of current state.
+ const state = new tr.ui.b.UIState(loc,
+ this.viewport_.currentDisplayTransform.scaleX);
+ this.timelineView_.setFindCtlText(
+ state.toUserFriendlyString(this.viewport_));
+ }.bind(this),
+ undefined, // Mouse up handler.
+ function onKeyUpDuringDrag() {
+ if (this.dragBeginEvent_) {
+ this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
+ this.dragBoxXEnd_, this.dragBoxYEnd_);
+ }
+ }.bind(this));
+ },
+
+ queueSmoothPan_(viewDeltaX, deltaY) {
+ const deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld(
+ viewDeltaX);
+ const animation = new tr.ui.TimelineDisplayTransformPanAnimation(
+ deltaX, deltaY);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+ },
+
+ /**
+ * Zoom in or out on the timeline by the given scale factor.
+ * @param {Number} scale The scale factor to apply. If <1, zooms out.
+ * @param {boolean} Whether to change the zoom level smoothly.
+ */
+ zoomBy_(scale, smooth) {
+ if (scale <= 0) {
+ return;
+ }
+
+ smooth = !!smooth;
+ const vp = this.viewport_;
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio;
+ const goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld(
+ goalFocalPointXView);
+ if (smooth) {
+ const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
+ goalFocalPointXWorld, goalFocalPointXView,
+ vp.currentDisplayTransform.panY,
+ scale);
+ vp.queueDisplayTransformAnimation(animation);
+ } else {
+ this.displayTransform_.set(vp.currentDisplayTransform);
+ this.displayTransform_.scaleX *= scale;
+ this.displayTransform_.xPanWorldPosToViewPos(
+ goalFocalPointXWorld, goalFocalPointXView, this.viewWidth_);
+ vp.setDisplayTransformImmediately(this.displayTransform_);
+ }
+ },
+
+ /**
+ * Zoom into the current selection.
+ */
+ zoomToSelection() {
+ if (!this.brushingStateController.selectionOfInterest.length) return;
+
+ const bounds = this.brushingStateController.selectionOfInterest.bounds;
+ if (!bounds.range) return;
+
+ const worldCenter = bounds.center;
+ const viewCenter = this.modelTrackContainer_.canvas.width / 2;
+ const adjustedWorldRange = bounds.range * 1.25;
+ const newScale = this.modelTrackContainer_.canvas.width /
+ adjustedWorldRange;
+ const zoomInRatio = newScale /
+ this.viewport_.currentDisplayTransform.scaleX;
+
+ const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
+ worldCenter, viewCenter,
+ this.viewport_.currentDisplayTransform.panY,
+ zoomInRatio);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+ },
+
+ /**
+ * Pan the view so the current selection becomes visible.
+ */
+ panToSelection() {
+ if (!this.brushingStateController.selectionOfInterest.length) return;
+
+ const bounds = this.brushingStateController.selectionOfInterest.bounds;
+ const worldCenter = bounds.center;
+ const viewWidth = this.viewWidth_;
+
+ const dt = this.viewport_.currentDisplayTransform;
+ if (false && !bounds.range) {
+ if (dt.xWorldToView(bounds.center) < 0 ||
+ dt.xWorldToView(bounds.center) > viewWidth) {
+ this.displayTransform_.set(dt);
+ this.displayTransform_.xPanWorldPosToViewPos(
+ worldCenter, 'center', viewWidth);
+ const deltaX = this.displayTransform_.panX - dt.panX;
+ const animation = new tr.ui.TimelineDisplayTransformPanAnimation(
+ deltaX, 0);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+ }
+ return;
+ }
+
+ this.displayTransform_.set(dt);
+ this.displayTransform_.xPanWorldBoundsIntoView(
+ bounds.min,
+ bounds.max,
+ viewWidth);
+ const deltaX = this.displayTransform_.panX - dt.panX;
+ const animation = new tr.ui.TimelineDisplayTransformPanAnimation(
+ deltaX, 0);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+ },
+
+ navToPosition(uiState, showNavLine) {
+ const location = uiState.location;
+ const scaleX = uiState.scaleX;
+ const track = location.getContainingTrack(this.viewport_);
+
+ const worldCenter = location.xWorld;
+ const viewCenter = this.modelTrackContainer_.canvas.width / 5;
+ const zoomInRatio = scaleX /
+ this.viewport_.currentDisplayTransform.scaleX;
+
+ // Vertically scroll so track is in view.
+ track.scrollIntoViewIfNeeded();
+
+ // Perform zoom and panX animation.
+ const animation = new tr.ui.TimelineDisplayTransformZoomToAnimation(
+ worldCenter, viewCenter,
+ this.viewport_.currentDisplayTransform.panY,
+ zoomInRatio);
+ this.viewport_.queueDisplayTransformAnimation(animation);
+
+ if (!showNavLine) return;
+ // Add an X Marker Annotation at the specified timestamp.
+ if (this.xNavStringMarker_) {
+ this.model.removeAnnotation(this.xNavStringMarker_);
+ }
+ this.xNavStringMarker_ =
+ new tr.model.XMarkerAnnotation(worldCenter);
+ this.model.addAnnotation(this.xNavStringMarker_);
+ },
+
+ selectPowerSamplesInCurrentTimeRange_() {
+ const selectionBounds = this.brushingStateController_.selection.bounds;
+ if (this.model.device.powerSeries && !selectionBounds.empty) {
+ const events = this.model.device.powerSeries.getSamplesWithinRange(
+ selectionBounds.min, selectionBounds.max);
+ const selection = new tr.model.EventSet(events);
+ this.brushingStateController_.changeSelectionFromTimeline(selection);
+ }
+ },
+
+ setCurrentSelectionAsInterestRange_() {
+ const selectionBounds = this.brushingStateController_.selection.bounds;
+ if (selectionBounds.empty) {
+ this.viewport_.interestRange.reset();
+ return;
+ }
+
+ if (this.viewport_.interestRange.min === selectionBounds.min &&
+ this.viewport_.interestRange.max === selectionBounds.max) {
+ this.viewport_.interestRange.reset();
+ } else {
+ this.viewport_.interestRange.set(selectionBounds);
+ }
+ },
+
+ toggleHighDetails_() {
+ this.viewport_.highDetails = !this.viewport_.highDetails;
+ },
+
+ hideDragBox_() {
+ this.$.drag_box.style.left = '-1000px';
+ this.$.drag_box.style.top = '-1000px';
+ this.$.drag_box.style.width = 0;
+ this.$.drag_box.style.height = 0;
+ },
+
+ setDragBoxPosition_(xStart, yStart, xEnd, yEnd) {
+ const loY = Math.min(yStart, yEnd);
+ const hiY = Math.max(yStart, yEnd);
+ const loX = Math.min(xStart, xEnd);
+ const hiX = Math.max(xStart, xEnd);
+ const modelTrackRect = this.modelTrack_.getBoundingClientRect();
+ const dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY};
+
+ dragRect.right = dragRect.left + dragRect.width;
+ dragRect.bottom = dragRect.top + dragRect.height;
+
+ const modelTrackContainerRect =
+ this.modelTrackContainer_.getBoundingClientRect();
+ const clipRect = {
+ left: modelTrackContainerRect.left,
+ top: modelTrackContainerRect.top,
+ right: modelTrackContainerRect.right,
+ bottom: modelTrackContainerRect.bottom
+ };
+
+ const headingWidth = window.getComputedStyle(
+ Polymer.dom(this).querySelector('tr-ui-b-heading')).width;
+ const trackTitleWidth = parseInt(headingWidth);
+ clipRect.left = clipRect.left + trackTitleWidth;
+
+ const intersectRect_ = function(r1, r2) {
+ if (r2.left > r1.right || r2.right < r1.left ||
+ r2.top > r1.bottom || r2.bottom < r1.top) {
+ return false;
+ }
+
+ const results = {};
+ results.left = Math.max(r1.left, r2.left);
+ results.top = Math.max(r1.top, r2.top);
+ results.right = Math.min(r1.right, r2.right);
+ results.bottom = Math.min(r1.bottom, r2.bottom);
+ results.width = results.right - results.left;
+ results.height = results.bottom - results.top;
+ return results;
+ };
+
+ // TODO(dsinclair): intersectRect_ can return false (which should actually
+ // be undefined) but we use finalDragBox without checking the return value
+ // which could potentially blowup. Fix this .....
+ const finalDragBox = intersectRect_(clipRect, dragRect);
+
+ this.$.drag_box.style.left = finalDragBox.left + 'px';
+ this.$.drag_box.style.width = finalDragBox.width + 'px';
+ this.$.drag_box.style.top = finalDragBox.top + 'px';
+ this.$.drag_box.style.height = finalDragBox.height + 'px';
+ this.$.drag_box.style.whiteSpace = 'nowrap';
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const canv = this.modelTrackContainer_.canvas;
+ const dt = this.viewport_.currentDisplayTransform;
+ const loWX = dt.xViewToWorld(
+ (loX - canv.offsetLeft) * pixelRatio);
+ const hiWX = dt.xViewToWorld(
+ (hiX - canv.offsetLeft) * pixelRatio);
+
+ Polymer.dom(this.$.drag_box).textContent =
+ tr.b.Unit.byName.timeDurationInMs.format(hiWX - loWX);
+
+ const e = new tr.b.Event('selectionChanging');
+ e.loWX = loWX;
+ e.hiWX = hiWX;
+ this.dispatchEvent(e);
+ },
+
+ onGridToggle_(left) {
+ const selection = this.brushingStateController_.selection;
+ const tb = left ? selection.bounds.min : selection.bounds.max;
+
+ // Toggle the grid off if the grid is on, the marker position is the same
+ // and the same element is selected (same timebase).
+ if (this.viewport_.gridEnabled &&
+ this.viewport_.gridSide === left &&
+ this.viewport_.gridInitialTimebase === tb) {
+ this.viewport_.gridside = undefined;
+ this.viewport_.gridEnabled = false;
+ this.viewport_.gridInitialTimebase = undefined;
+ return;
+ }
+
+ // Shift the timebase left until its just left of model_.bounds.min.
+ const numIntervalsSinceStart = Math.ceil((tb - this.model_.bounds.min) /
+ this.viewport_.gridStep_);
+
+ this.viewport_.gridEnabled = true;
+ this.viewport_.gridSide = left;
+ this.viewport_.gridInitialTimebase = tb;
+ this.viewport_.gridTimebase = tb -
+ (numIntervalsSinceStart + 1) * this.viewport_.gridStep_;
+ },
+
+ storeLastMousePos_(e) {
+ this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+ },
+
+ storeLastTouchPositions_(e) {
+ this.lastTouchViewPositions_ = this.extractRelativeTouchPositions_(e);
+ },
+
+ extractRelativeMousePosition_(e) {
+ const canv = this.modelTrackContainer_.canvas;
+ return {
+ x: e.clientX - canv.offsetLeft,
+ y: e.clientY - canv.offsetTop
+ };
+ },
+
+ extractRelativeTouchPositions_(e) {
+ const canv = this.modelTrackContainer_.canvas;
+
+ const touches = [];
+ for (let i = 0; i < e.touches.length; ++i) {
+ touches.push({
+ x: e.touches[i].clientX - canv.offsetLeft,
+ y: e.touches[i].clientY - canv.offsetTop
+ });
+ }
+ return touches;
+ },
+
+ storeInitialMouseDownPos_(e) {
+ const position = this.extractRelativeMousePosition_(e);
+
+ this.mouseViewPosAtMouseDown_.x = position.x;
+ this.mouseViewPosAtMouseDown_.y = position.y;
+ },
+
+ focusElements_() {
+ this.$.hotkey_controller.childRequestsGeneralFocus(this);
+ },
+
+ storeInitialInteractionPositionsAndFocus_(e) {
+ this.storeInitialMouseDownPos_(e);
+ this.storeLastMousePos_(e);
+
+ this.focusElements_();
+ },
+
+ onBeginPanScan_(e) {
+ const vp = this.viewport_;
+ this.viewportDisplayTransformAtMouseDown_ =
+ vp.currentDisplayTransform.clone();
+ this.isPanningAndScanning_ = true;
+
+ this.storeInitialInteractionPositionsAndFocus_(e);
+ e.preventDefault();
+ },
+
+ onUpdatePanScan_(e) {
+ if (!this.isPanningAndScanning_) return;
+
+ const viewWidth = this.viewWidth_;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const xDeltaView = pixelRatio * (this.lastMouseViewPos_.x -
+ this.mouseViewPosAtMouseDown_.x);
+
+ const yDelta = this.lastMouseViewPos_.y -
+ this.mouseViewPosAtMouseDown_.y;
+
+ this.displayTransform_.set(this.viewportDisplayTransformAtMouseDown_);
+ this.displayTransform_.incrementPanXInViewUnits(xDeltaView);
+ this.displayTransform_.panY -= yDelta;
+ this.viewport_.setDisplayTransformImmediately(this.displayTransform_);
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.storeLastMousePos_(e);
+ },
+
+ onEndPanScan_(e) {
+ this.isPanningAndScanning_ = false;
+
+ this.storeLastMousePos_(e);
+
+ if (!e.isClick) {
+ e.preventDefault();
+ }
+ },
+
+ onBeginSelection_(e) {
+ const canv = this.modelTrackContainer_.canvas;
+ const rect = this.modelTrack_.getBoundingClientRect();
+ const canvRect = canv.getBoundingClientRect();
+
+ const inside = rect &&
+ e.clientX >= rect.left &&
+ e.clientX < rect.right &&
+ e.clientY >= rect.top &&
+ e.clientY < rect.bottom &&
+ e.clientX >= canvRect.left &&
+ e.clientX < canvRect.right;
+
+ if (!inside) return;
+
+ this.dragBeginEvent_ = e;
+
+ this.storeInitialInteractionPositionsAndFocus_(e);
+ e.preventDefault();
+ },
+
+ onUpdateSelection_(e) {
+ if (!this.dragBeginEvent_) return;
+
+ // Update the drag box
+ this.dragBoxXStart_ = this.dragBeginEvent_.clientX;
+ this.dragBoxXEnd_ = e.clientX;
+ this.dragBoxYStart_ = this.dragBeginEvent_.clientY;
+ this.dragBoxYEnd_ = e.clientY;
+ this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
+ this.dragBoxXEnd_, this.dragBoxYEnd_);
+ },
+
+ onEndSelection_(e) {
+ e.preventDefault();
+
+ if (!this.dragBeginEvent_) return;
+
+ // Stop the dragging.
+ this.hideDragBox_();
+ const eDown = this.dragBeginEvent_;
+ this.dragBeginEvent_ = undefined;
+
+ // Figure out extents of the drag.
+ const loY = Math.min(eDown.clientY, e.clientY);
+ const hiY = Math.max(eDown.clientY, e.clientY);
+ const loX = Math.min(eDown.clientX, e.clientX);
+ const hiX = Math.max(eDown.clientX, e.clientX);
+
+ // Convert to worldspace.
+ const canv = this.modelTrackContainer_.canvas;
+ const worldOffset = canv.getBoundingClientRect().left;
+ const loVX = loX - worldOffset;
+ const hiVX = hiX - worldOffset;
+
+ // Figure out what has been selected.
+ const selection = new tr.model.EventSet();
+ if (eDown.appendSelection) {
+ const previousSelection = this.brushingStateController_.selection;
+ if (previousSelection !== undefined) {
+ selection.addEventSet(previousSelection);
+ }
+ }
+ this.modelTrack_.addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection);
+
+ // Activate the new selection.
+ this.brushingStateController_.changeSelectionFromTimeline(selection);
+ },
+
+ onBeginZoom_(e) {
+ this.isZooming_ = true;
+
+ this.storeInitialInteractionPositionsAndFocus_(e);
+ e.preventDefault();
+ },
+
+ onUpdateZoom_(e) {
+ if (!this.isZooming_) return;
+
+ const newPosition = this.extractRelativeMousePosition_(e);
+
+ const zoomScaleValue = 1 + (this.lastMouseViewPos_.y -
+ newPosition.y) * 0.01;
+
+ this.zoomBy_(zoomScaleValue, false);
+ this.storeLastMousePos_(e);
+ },
+
+ onEndZoom_(e) {
+ this.isZooming_ = false;
+
+ if (!e.isClick) {
+ e.preventDefault();
+ }
+ },
+
+ computeTouchCenter_(positions) {
+ let xSum = 0;
+ let ySum = 0;
+ for (let i = 0; i < positions.length; ++i) {
+ xSum += positions[i].x;
+ ySum += positions[i].y;
+ }
+ return {
+ x: xSum / positions.length,
+ y: ySum / positions.length
+ };
+ },
+
+ computeTouchSpan_(positions) {
+ let xMin = Number.MAX_VALUE;
+ let yMin = Number.MAX_VALUE;
+ let xMax = Number.MIN_VALUE;
+ let yMax = Number.MIN_VALUE;
+ for (let i = 0; i < positions.length; ++i) {
+ xMin = Math.min(xMin, positions[i].x);
+ yMin = Math.min(yMin, positions[i].y);
+ xMax = Math.max(xMax, positions[i].x);
+ yMax = Math.max(yMax, positions[i].y);
+ }
+ return Math.sqrt((xMin - xMax) * (xMin - xMax) +
+ (yMin - yMax) * (yMin - yMax));
+ },
+
+ onUpdateTransformForTouch_(e) {
+ const newPositions = this.extractRelativeTouchPositions_(e);
+ const currentPositions = this.lastTouchViewPositions_;
+
+ const newCenter = this.computeTouchCenter_(newPositions);
+ const currentCenter = this.computeTouchCenter_(currentPositions);
+
+ const newSpan = this.computeTouchSpan_(newPositions);
+ const currentSpan = this.computeTouchSpan_(currentPositions);
+
+ const vp = this.viewport_;
+ const viewWidth = this.viewWidth_;
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const xDelta = pixelRatio * (newCenter.x - currentCenter.x);
+ const yDelta = newCenter.y - currentCenter.y;
+ const zoomScaleValue = currentSpan > 10 ? newSpan / currentSpan : 1;
+
+ const viewFocus = pixelRatio * newCenter.x;
+ const worldFocus = vp.currentDisplayTransform.xViewToWorld(viewFocus);
+
+ this.displayTransform_.set(vp.currentDisplayTransform);
+ this.displayTransform_.scaleX *= zoomScaleValue;
+ this.displayTransform_.xPanWorldPosToViewPos(
+ worldFocus, viewFocus, viewWidth);
+ this.displayTransform_.incrementPanXInViewUnits(xDelta);
+ this.displayTransform_.panY -= yDelta;
+ vp.setDisplayTransformImmediately(this.displayTransform_);
+ this.storeLastTouchPositions_(e);
+ },
+
+ initHintText_() {
+ this.$.hint_text.style.display = 'none';
+
+ this.pendingHintTextClearTimeout_ = undefined;
+ },
+
+ showHintText_(text) {
+ if (this.pendingHintTextClearTimeout_) {
+ window.clearTimeout(this.pendingHintTextClearTimeout_);
+ this.pendingHintTextClearTimeout_ = undefined;
+ }
+ this.pendingHintTextClearTimeout_ = setTimeout(
+ this.hideHintText_.bind(this), 1000);
+ Polymer.dom(this.$.hint_text).textContent = text;
+ this.$.hint_text.style.display = '';
+ },
+
+ hideHintText_() {
+ this.pendingHintTextClearTimeout_ = undefined;
+ this.$.hint_text.style.display = 'none';
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html
new file mode 100644
index 00000000000..a84addbe9bc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_track_view_test.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Task = tr.b.Task;
+
+ test('instantiate', function() {
+ const numThreads = 500;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ const p100 = model.getOrCreateProcess(100);
+ for (let i = 0; i < numThreads; i++) {
+ const t = p100.getOrCreateThread(101 + i);
+ if (i % 2 === 0) {
+ t.sliceGroup.beginSlice('cat', 'a', 100);
+ t.sliceGroup.endSlice(110);
+ } else {
+ t.sliceGroup.beginSlice('cat', 'b', 50);
+ t.sliceGroup.endSlice(120);
+ }
+ }
+ }
+ });
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+ timeline.style.maxHeight = '600px';
+ this.addHTMLOutput(timeline);
+ });
+
+ test('addAllEventsMatchingFilterToSelectionAsTask', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'b', 0, 1.1, {}, 2.8));
+
+ const t1asg = t1.asyncSliceGroup;
+ t1asg.slices.push(
+ tr.c.TestUtils.newAsyncSliceNamed('a', 0, 1, t1, t1));
+ t1asg.slices.push(
+ tr.c.TestUtils.newAsyncSliceNamed('b', 1, 2, t1, t1));
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ let expected = new tr.model.EventSet(
+ [t1asg.slices[0], t1.sliceGroup.slices[0]]);
+ let result = new tr.model.EventSet;
+ let filterTask = timeline.addAllEventsMatchingFilterToSelectionAsTask(
+ new tr.c.TitleOrCategoryFilter('a'), result);
+ Task.RunSynchronously(filterTask);
+ assert.isTrue(result.equals(expected));
+
+ expected = new tr.model.EventSet(
+ [t1asg.slices[1], t1.sliceGroup.slices[1]]);
+ result = new tr.model.EventSet();
+ filterTask = timeline.addAllEventsMatchingFilterToSelectionAsTask(
+ new tr.c.TitleOrCategoryFilter('b'), result);
+ Task.RunSynchronously(filterTask);
+ assert.isTrue(result.equals(expected));
+ });
+
+ test('emptyThreadsDeleted', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isFalse(timeline.hasVisibleContent);
+ });
+
+ test('filteredCounters', function() {
+ const model = new tr.Model();
+ const c1 = model.kernel.getOrCreateCpu(0);
+ c1.getOrCreateCounter('', 'b');
+
+ const p1 = model.getOrCreateProcess(1);
+ const ctr = p1.getOrCreateCounter('', 'a');
+ const series = new tr.model.CounterSeries('a', 0);
+ series.addCounterSample(0, 1);
+ ctr.addSeries(series);
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isTrue(timeline.hasVisibleContent);
+ });
+
+ test('filteredCpus', function() {
+ const model = new tr.Model();
+ const c1 = model.kernel.getOrCreateCpu(1);
+ c1.getOrCreateCounter('', 'a');
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isTrue(timeline.hasVisibleContent);
+ });
+
+ test('filteredProcesses', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ p1.getOrCreateCounter('', 'a');
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isTrue(timeline.hasVisibleContent);
+ });
+
+ test('filteredThreads', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(2);
+ t1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({start: 0, duration: 1}));
+
+ const timeline = document.createElement('tr-ui-timeline-track-view');
+ timeline.model = model;
+
+ assert.isTrue(timeline.hasVisibleContent);
+ });
+
+ test('interestRange', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ const model = tr.c.TestUtils.newModelWithEvents([events]);
+ const trackView = document.createElement('tr-ui-timeline-track-view');
+ trackView.model = model;
+ this.addHTMLOutput(trackView);
+
+ const slice = model.processes[52].threads[53].sliceGroup.slices[2];
+ trackView.viewport.interestRange.setMinAndMax(slice.start, slice.end);
+ });
+
+ test('emptyInterestRange', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ const model = tr.c.TestUtils.newModelWithEvents([events]);
+ const trackView = document.createElement('tr-ui-timeline-track-view');
+ trackView.model = model;
+ this.addHTMLOutput(trackView);
+ trackView.viewport.interestRange.reset();
+ });
+
+
+ test('thinnestInterestRange', function() {
+ const events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+ const model = tr.c.TestUtils.newModelWithEvents([events]);
+ const trackView = document.createElement('tr-ui-timeline-track-view');
+ trackView.model = model;
+ this.addHTMLOutput(trackView);
+ trackView.viewport.interestRange.reset();
+
+ const slice = model.processes[52].threads[53].sliceGroup.slices[2];
+ trackView.viewport.interestRange.setMinAndMax(slice.start, slice.start);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html
new file mode 100644
index 00000000000..9a7a83f4a7b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view.html
@@ -0,0 +1,641 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/settings.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/scripting_controller.html">
+<link rel="import" href="/tracing/metrics/all_metrics.html">
+<link rel="import" href="/tracing/ui/analysis/analysis_view.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/drag_handle.html">
+<link rel="import" href="/tracing/ui/base/dropdown.html">
+<link rel="import" href="/tracing/ui/base/favicons.html">
+<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
+<link rel="import" href="/tracing/ui/base/info_bar_group.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/toolbar_button.html">
+<link rel="import" href="/tracing/ui/base/utils.html">
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/find_control.html">
+<link rel="import" href="/tracing/ui/find_controller.html">
+<link rel="import" href="/tracing/ui/scripting_control.html">
+<link rel="import" href="/tracing/ui/side_panel/side_panel_container.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/timeline_view_help_overlay.html">
+<link rel="import" href="/tracing/ui/timeline_view_metadata_overlay.html">
+<link rel="import" href="/tracing/value/ui/preferred_display_unit.html">
+
+<dom-module id='tr-ui-timeline-view'>
+ <template>
+ <style>
+ :host {
+ flex-direction: column;
+ cursor: default;
+ display: flex;
+ font-family: sans-serif;
+ padding: 0;
+ }
+
+ #control {
+ background-color: #e6e6e6;
+ background-image: -webkit-gradient(linear, 0 0, 0 100%,
+ from(#E5E5E5), to(#D1D1D1));
+ flex: 0 0 auto;
+ overflow-x: auto;
+ }
+
+ #control::-webkit-scrollbar { height: 0px; }
+
+ #control > #bar {
+ font-size: 12px;
+ display: flex;
+ flex-direction: row;
+ margin: 1px;
+ }
+
+ #control > #bar > #title {
+ display: flex;
+ align-items: center;
+ padding-left: 8px;
+ padding-right: 8px;
+ flex: 1 1 auto;
+ }
+
+ #control > #bar > #left_controls,
+ #control > #bar > #right_controls {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ }
+
+ #control > #bar > #left_controls > * { margin-right: 2px; }
+ #control > #bar > #right_controls > * { margin-left: 2px; }
+ #control > #collapsing_controls { display: flex; }
+
+ middle-container {
+ flex: 1 1 auto;
+ flex-direction: row;
+ border-bottom: 1px solid #8e8e8e;
+ display: flex;
+ min-height: 0;
+ }
+
+ middle-container ::content track-view-container {
+ flex: 1 1 auto;
+ display: flex;
+ min-height: 0;
+ min-width: 0;
+ overflow-x: hidden;
+ }
+
+ middle-container ::content track-view-container > * { flex: 1 1 auto; }
+ middle-container > x-timeline-view-side-panel-container { flex: 0 0 auto; }
+ tr-ui-b-drag-handle { flex: 0 0 auto; }
+ tr-ui-a-analysis-view { flex: 0 0 auto; }
+
+ #view_options_dropdown, #process_filter_dropdown {
+ --dropdown-button: {
+ -webkit-appearance: none;
+ align-items: normal;
+ background-color: rgb(248, 248, 248);
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ box-sizing: content-box;
+ color: rgba(0, 0, 0, 0.8);
+ font-family: sans-serif;
+ font-size: 12px;
+ padding: 2px 5px;
+ }
+ }
+ </style>
+
+ <tv-ui-b-hotkey-controller id="hkc"></tv-ui-b-hotkey-controller>
+ <div id="control">
+ <div id="bar">
+ <div id="left_controls"></div>
+ <div id="title">^_^</div>
+ <div id="right_controls">
+ <tr-ui-b-dropdown id="process_filter_dropdown" label="Processes"></tr-ui-b-dropdown>
+ <tr-ui-b-toolbar-button id="view_metadata_button">
+ M
+ </tr-ui-b-toolbar-button>
+ <tr-ui-b-dropdown id="view_options_dropdown" label="View Options"></tr-ui-b-dropdown>
+ <tr-ui-find-control id="view_find_control"></tr-ui-find-control>
+ <tr-ui-b-toolbar-button id="view_console_button">
+ &#187;
+ </tr-ui-b-toolbar-button>
+ <tr-ui-b-toolbar-button id="view_help_button">
+ ?
+ </tr-ui-b-toolbar-button>
+ </div>
+ </div>
+ <div id="collapsing_controls"></div>
+ <tr-ui-b-info-bar-group id="import-warnings">
+ </tr-ui-b-info-bar-group>
+ </div>
+ <middle-container>
+ <slot></slot>
+
+ <tr-ui-side-panel-container id="side_panel_container">
+ </tr-ui-side-panel-container>
+ </middle-container>
+ <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle>
+ <tr-ui-a-analysis-view id="analysis"></tr-ui-a-analysis-view>
+
+ <tr-v-ui-preferred-display-unit id="display_unit">
+ </tr-v-ui-preferred-display-unit>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-timeline-view',
+
+ created() {
+ this.trackViewContainer_ = undefined;
+
+ this.queuedModel_ = undefined;
+
+ this.builtPromise_ = undefined;
+ this.doneBuilding_ = undefined;
+ },
+
+ attached() {
+ this.async(function() {
+ this.trackViewContainer_ = Polymer.dom(this).querySelector(
+ '#track_view_container');
+ if (!this.trackViewContainer_) {
+ throw new Error('missing trackviewContainer');
+ }
+
+ if (this.queuedModel_) this.updateContents_();
+ });
+ },
+
+ ready() {
+ this.tabIndex = 0; // Let the timeline able to receive key events.
+
+ this.titleEl_ = this.$.title;
+ this.leftControlsEl_ = this.$.left_controls;
+ this.rightControlsEl_ = this.$.right_controls;
+ this.collapsingControlsEl_ = this.$.collapsing_controls;
+ this.sidePanelContainer_ = this.$.side_panel_container;
+
+ this.brushingStateController_ = new tr.c.BrushingStateController(this);
+
+ this.findCtl_ = this.$.view_find_control;
+ this.findCtl_.controller = new tr.ui.FindController(
+ this.brushingStateController_);
+
+ this.scriptingCtl_ = document.createElement('tr-ui-scripting-control');
+ this.scriptingCtl_.controller = new tr.c.ScriptingController(
+ this.brushingStateController_);
+
+ this.sidePanelContainer_.brushingStateController =
+ this.brushingStateController_;
+
+ if (window.tr.metrics && window.tr.metrics.sh &&
+ window.tr.metrics.sh.SystemHealthMetric) {
+ this.railScoreSpan_ = document.createElement(
+ 'tr-metrics-ui-sh-system-health-span');
+ Polymer.dom(this.rightControls).appendChild(this.railScoreSpan_);
+ } else {
+ this.railScoreSpan_ = undefined;
+ }
+
+ this.processFilter_ = this.$.process_filter_dropdown;
+
+ this.optionsDropdown_ = this.$.view_options_dropdown;
+ Polymer.dom(this.optionsDropdown_.iconElement).textContent = 'View Options';
+
+ this.showFlowEvents_ = false;
+ Polymer.dom(this.optionsDropdown_).appendChild(tr.ui.b.createCheckBox(
+ this, 'showFlowEvents',
+ 'tr.ui.TimelineView.showFlowEvents', false,
+ 'Flow events'));
+ this.highlightVSync_ = false;
+ this.highlightVSyncCheckbox_ = tr.ui.b.createCheckBox(
+ this, 'highlightVSync',
+ 'tr.ui.TimelineView.highlightVSync', false,
+ 'Highlight VSync');
+ Polymer.dom(this.optionsDropdown_).appendChild(
+ this.highlightVSyncCheckbox_);
+
+ this.initMetadataButton_();
+ this.initConsoleButton_();
+ this.initHelpButton_();
+
+ Polymer.dom(this.collapsingControls).appendChild(this.scriptingCtl_);
+
+ this.dragEl_ = this.$.drag_handle;
+
+ this.analysisEl_ = this.$.analysis;
+ this.analysisEl_.brushingStateController = this.brushingStateController_;
+
+ this.addEventListener(
+ 'requestSelectionChange',
+ function(e) {
+ const sc = this.brushingStateController_;
+ sc.changeSelectionFromRequestSelectionChangeEvent(e.selection);
+ }.bind(this));
+
+ // Bookkeeping.
+ this.onViewportChanged_ = this.onViewportChanged_.bind(this);
+ this.bindKeyListeners_();
+
+ this.dragEl_.target = this.analysisEl_;
+ },
+
+ get globalMode() {
+ return this.hotkeyController.globalMode;
+ },
+
+ set globalMode(globalMode) {
+ globalMode = !!globalMode;
+ this.brushingStateController_.historyEnabled = globalMode;
+ this.hotkeyController.globalMode = globalMode;
+ },
+
+ get hotkeyController() {
+ return this.$.hkc;
+ },
+
+ updateDocumentFavicon() {
+ let hue;
+ if (!this.model) {
+ hue = 'blue';
+ } else {
+ hue = this.model.faviconHue;
+ }
+
+ let faviconData = tr.ui.b.FaviconsByHue[hue];
+ if (faviconData === undefined) {
+ faviconData = tr.ui.b.FaviconsByHue.blue;
+ }
+
+ // Find link if its there
+ let link = Polymer.dom(document.head).querySelector(
+ 'link[rel="shortcut icon"]');
+ if (!link) {
+ link = document.createElement('link');
+ link.rel = 'shortcut icon';
+ Polymer.dom(document.head).appendChild(link);
+ }
+ link.href = faviconData;
+ },
+
+ get showFlowEvents() {
+ return this.showFlowEvents_;
+ },
+
+ set showFlowEvents(showFlowEvents) {
+ this.showFlowEvents_ = showFlowEvents;
+ if (!this.trackView_) return;
+
+ this.trackView_.viewport.showFlowEvents = showFlowEvents;
+ },
+
+ get highlightVSync() {
+ return this.highlightVSync_;
+ },
+
+ set highlightVSync(highlightVSync) {
+ this.highlightVSync_ = highlightVSync;
+ if (!this.trackView_) return;
+
+ this.trackView_.viewport.highlightVSync = highlightVSync;
+ },
+
+ initHelpButton_() {
+ const helpButtonEl = this.$.view_help_button;
+
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'Chrome Tracing Help';
+ dlg.visible = false;
+ dlg.appendChild(
+ document.createElement('tr-ui-timeline-view-help-overlay'));
+
+ function onClick(e) {
+ dlg.visible = !dlg.visible;
+ // Stop event so it doesn't trigger new click listener on document.
+ e.stopPropagation();
+ }
+
+ helpButtonEl.addEventListener('click', onClick.bind(this));
+ },
+
+ initConsoleButton_() {
+ const toggleEl = this.$.view_console_button;
+
+ function onClick(e) {
+ this.scriptingCtl_.toggleVisibility();
+ e.stopPropagation();
+ return false;
+ }
+ toggleEl.addEventListener('click', onClick.bind(this));
+ },
+
+ initMetadataButton_() {
+ const showEl = this.$.view_metadata_button;
+
+ function onClick(e) {
+ const dlg = new tr.ui.b.Overlay();
+ dlg.title = 'Metadata for trace';
+
+ const metadataOverlay = document.createElement(
+ 'tr-ui-timeline-view-metadata-overlay');
+ metadataOverlay.metadata = this.model.metadata;
+
+ Polymer.dom(dlg).appendChild(metadataOverlay);
+ dlg.visible = true;
+
+ e.stopPropagation();
+ return false;
+ }
+ showEl.addEventListener('click', onClick.bind(this));
+
+ this.updateMetadataButtonVisibility_();
+ },
+
+ updateMetadataButtonVisibility_() {
+ const showEl = this.$.view_metadata_button;
+ showEl.style.display =
+ (this.model && this.model.metadata.length) ? '' : 'none';
+ },
+
+ updateProcessList_() {
+ const dropdown = Polymer.dom(this.processFilter_);
+ while (dropdown.firstChild) {
+ dropdown.removeChild(dropdown.firstChild);
+ }
+ if (!this.model) return;
+
+ const trackView =
+ this.trackViewContainer_.querySelector('tr-ui-timeline-track-view');
+ const processViews = trackView.processViews;
+ const cboxes = [];
+ const updateAll = (checked) => {
+ for (const cbox of cboxes) {
+ cbox.checked = checked;
+ }
+ };
+
+ dropdown.appendChild(tr.ui.b.createButton('All', () => updateAll(true)));
+ dropdown.appendChild(tr.ui.b.createButton('None', () => updateAll(false)));
+
+ for (const view of processViews) {
+ const cbox = tr.ui.b.createCheckBox(undefined, undefined, undefined,
+ true, view.processBase.userFriendlyName,
+ () => view.visible = cbox.checked);
+ cbox.checked = view.visible;
+ cboxes.push(cbox);
+ view.addEventListener('visibility', () => cbox.checked = view.visible);
+ dropdown.appendChild(cbox);
+ }
+ },
+
+ get leftControls() {
+ return this.leftControlsEl_;
+ },
+
+ get rightControls() {
+ return this.rightControlsEl_;
+ },
+
+ get collapsingControls() {
+ return this.collapsingControlsEl_;
+ },
+
+ get viewTitle() {
+ return Polymer.dom(this.titleEl_).textContent.substring(
+ Polymer.dom(this.titleEl_).textContent.length - 2);
+ },
+
+ set viewTitle(text) {
+ if (text === undefined) {
+ Polymer.dom(this.titleEl_).textContent = '';
+ this.titleEl_.hidden = true;
+ return;
+ }
+ this.titleEl_.hidden = false;
+ Polymer.dom(this.titleEl_).textContent = text;
+ },
+
+ get model() {
+ if (this.trackView_) {
+ return this.trackView_.model;
+ }
+ return undefined;
+ },
+
+ set model(model) {
+ this.build(model);
+ },
+
+ async build(model) {
+ this.queuedModel_ = model;
+ this.builtPromise_ = new Promise((resolve, reject) => {
+ this.doneBuilding_ = resolve;
+ });
+ if (this.trackViewContainer_) await this.updateContents_();
+ },
+
+ get builtPromise() {
+ return this.builtPromise_;
+ },
+
+ async updateContents_() {
+ if (this.trackViewContainer_ === undefined) {
+ throw new Error(
+ 'timeline-view.updateContents_ requires trackViewContainer_');
+ }
+
+ const model = this.queuedModel_;
+ this.queuedModel_ = undefined;
+
+ const modelInstanceChanged = model !== this.model;
+ const modelValid = model && !model.bounds.isEmpty;
+
+ const importWarningsEl = Polymer.dom(this.root).querySelector(
+ '#import-warnings');
+ Polymer.dom(importWarningsEl).textContent = '';
+
+ // Remove old trackView if the model has completely changed.
+ if (modelInstanceChanged) {
+ if (this.railScoreSpan_) {
+ this.railScoreSpan_.model = undefined;
+ }
+ Polymer.dom(this.trackViewContainer_).textContent = '';
+ if (this.trackView_) {
+ this.trackView_.viewport.removeEventListener(
+ 'change', this.onViewportChanged_);
+ this.trackView_.brushingStateController = undefined;
+ this.trackView_.detach();
+ this.trackView_ = undefined;
+ }
+ this.brushingStateController_.modelWillChange();
+ }
+
+ // Create new trackView if needed.
+ if (modelValid && !this.trackView_) {
+ this.trackView_ = document.createElement('tr-ui-timeline-track-view');
+ this.trackView_.timelineView = this;
+
+ this.trackView.brushingStateController = this.brushingStateController_;
+
+ Polymer.dom(this.trackViewContainer_).appendChild(this.trackView_);
+ this.trackView_.viewport.addEventListener(
+ 'change', this.onViewportChanged_);
+ }
+
+ // Set the model.
+ if (modelValid) {
+ this.trackView_.model = model;
+ this.trackView_.viewport.showFlowEvents = this.showFlowEvents;
+ this.trackView_.viewport.highlightVSync = this.highlightVSync;
+ if (this.railScoreSpan_) {
+ this.railScoreSpan_.model = model;
+ }
+
+ this.$.display_unit.preferredTimeDisplayMode = model.intrinsicTimeUnit;
+ }
+
+ if (model) {
+ for (const warning of model.importWarningsThatShouldBeShownToUser) {
+ importWarningsEl.addMessage(
+ `Import Warning: ${warning.type}: ${warning.message}`, [{
+ buttonText: 'Dismiss',
+ onClick(event, infobar) {
+ infobar.visible = false;
+ }
+ }]);
+ }
+ }
+
+ // Do things that are selection specific
+ if (modelInstanceChanged) {
+ this.updateProcessList_();
+ this.updateMetadataButtonVisibility_();
+ this.brushingStateController_.modelDidChange();
+ this.onViewportChanged_();
+ }
+
+ this.doneBuilding_();
+ },
+
+ get brushingStateController() {
+ return this.brushingStateController_;
+ },
+
+ get trackView() {
+ return this.trackView_;
+ },
+
+ get settings() {
+ if (!this.settings_) {
+ this.settings_ = new tr.b.Settings();
+ }
+ return this.settings_;
+ },
+
+ /**
+ * Deprecated. Kept around because third_party code occasionally calls
+ * this to set up embedding.
+ */
+ set focusElement(value) {
+ throw new Error('This is deprecated. Please set globalMode to true.');
+ },
+
+ bindKeyListeners_() {
+ const hkc = this.hotkeyController;
+
+ // Shortcuts that *can* steal focus from the console and the filter text
+ // box.
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: '`'.charCodeAt(0),
+ useCapture: true,
+ thisArg: this,
+ callback(e) {
+ this.scriptingCtl_.toggleVisibility();
+ if (!this.scriptingCtl_.hasFocus) {
+ this.focus();
+ }
+ e.stopPropagation();
+ }
+ }));
+
+ // Shortcuts that *can* steal focus from the filter text box.
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: '/'.charCodeAt(0),
+ useCapture: true,
+ thisArg: this,
+ callback(e) {
+ if (this.scriptingCtl_.hasFocus) return;
+
+ if (this.findCtl_.hasFocus) {
+ this.focus();
+ } else {
+ this.findCtl_.focus();
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }));
+
+ // Shortcuts that *can't* steal focus.
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: '?'.charCodeAt(0),
+ useCapture: false,
+ thisArg: this,
+ callback(e) {
+ this.$.view_help_button.click();
+ e.stopPropagation();
+ }
+ }));
+
+ hkc.addHotKey(new tr.ui.b.HotKey({
+ eventType: 'keypress',
+ keyCode: 'v'.charCodeAt(0),
+ useCapture: false,
+ thisArg: this,
+ callback(e) {
+ this.toggleHighlightVSync_();
+ e.stopPropagation();
+ }
+ }));
+ },
+
+ onViewportChanged_(e) {
+ const spc = this.sidePanelContainer_;
+ if (!this.trackView_) {
+ spc.rangeOfInterest.reset();
+ return;
+ }
+
+ const vr = this.trackView_.viewport.interestRange.asRangeObject();
+ if (!spc.rangeOfInterest.equals(vr)) {
+ spc.rangeOfInterest = vr;
+ }
+
+ if (this.railScoreSpan_ && this.model) {
+ this.railScoreSpan_.model = this.model;
+ }
+ },
+
+ toggleHighlightVSync_() {
+ this.highlightVSyncCheckbox_.checked =
+ !this.highlightVSyncCheckbox_.checked;
+ },
+
+ setFindCtlText(string) {
+ this.findCtl_.setText(string);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html
new file mode 100644
index 00000000000..48c2e72df76
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay.html
@@ -0,0 +1,245 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+
+<dom-module id='tr-ui-timeline-view-help-overlay'>
+ <template>
+ <style>
+ :host {
+ flex: 1 1 auto;
+ flex-direction: row;
+ display: flex;
+ width: 700px;
+ }
+ .column {
+ width: 50%;
+ }
+ h2 {
+ font-size: 1.2em;
+ margin: 0;
+ margin-top: 5px;
+ text-align: center;
+ }
+ h3 {
+ margin: 0;
+ margin-left: 126px;
+ margin-top: 10px;
+ }
+ .pair {
+ flex: 1 1 auto;
+ flex-direction: row;
+ display: flex;
+ }
+ .command {
+ font-family: monospace;
+ margin-right: 5px;
+ text-align: right;
+ width: 150px;
+ }
+ .action {
+ font-size: 0.9em;
+ text-align: left;
+ width: 200px;
+ }
+ tr-ui-b-mouse-mode-icon {
+ border: 1px solid #888;
+ border-radius: 3px;
+ box-shadow: inset 0 0 2px rgba(0,0,0,0.3);
+ display: inline-block;
+ margin-right: 1px;
+ position: relative;
+ top: 4px;
+ }
+ .mouse-mode-icon.pan-mode {
+ background-position: -1px -11px;
+ }
+ .mouse-mode-icon.select-mode {
+ background-position: -1px -41px;
+ }
+ .mouse-mode-icon.zoom-mode {
+ background-position: -1px -71px;
+ }
+ .mouse-mode-icon.timing-mode {
+ background-position: -1px -101px;
+ }
+ </style>
+ <div class="column left">
+ <h2>Navigation</h2>
+ <div class='pair'>
+ <div class='command'>w/s</div>
+ <div class='action'>Zoom in/out (+shift: faster)</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>a/d</div>
+ <div class='action'>Pan left/right (+shift: faster)</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>&rarr;/shift-TAB</div>
+ <div class='action'>Select previous event</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>&larr;/TAB</div>
+ <div class='action'>Select next event</div>
+ </div>
+
+ <h2>Mouse Controls</h2>
+ <div class='pair'>
+ <div class='command'>click</div>
+ <div class='action'>Select event</div>
+ </div>
+ <div class='pair'>
+ <div class='command'>alt-mousewheel</div>
+ <div class='action'>Zoom in/out</div>
+ </div>
+
+ <h3>
+ <tr-ui-b-mouse-mode-icon mode-name="SELECTION"></tr-ui-b-mouse-mode-icon>
+ Select mode
+ </h3>
+ <div class='pair'>
+ <div class='command'>drag</div>
+ <div class='action'>Box select</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'><span class='mod'></span>-click/drag</div>
+ <div class='action'>Add events to the current selection</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>double click</div>
+ <div class='action'>Select all events with same title</div>
+ </div>
+
+ <h3>
+ <tr-ui-b-mouse-mode-icon mode-name="PANSCAN"></tr-ui-b-mouse-mode-icon>
+ Pan mode
+ </h3>
+ <div class='pair'>
+ <div class='command'>drag</div>
+ <div class='action'>Pan the view</div>
+ </div>
+
+ <h3>
+ <tr-ui-b-mouse-mode-icon mode-name="ZOOM"></tr-ui-b-mouse-mode-icon>
+ Zoom mode
+ </h3>
+ <div class='pair'>
+ <div class='command'>drag</div>
+ <div class='action'>Zoom in/out by dragging up/down</div>
+ </div>
+
+ <h3>
+ <tr-ui-b-mouse-mode-icon mode-name="TIMING"></tr-ui-b-mouse-mode-icon>
+ Timing mode
+ </h3>
+ <div class='pair'>
+ <div class='command'>drag</div>
+ <div class='action'>Create or move markers</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>double click</div>
+ <div class='action'>Set marker range to slice</div>
+ </div>
+ </div>
+
+ <div class="column right">
+ <h2>General</h2>
+ <div class='pair'>
+ <div class='command'>1-4</div>
+ <div class='action'>Switch mouse mode</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>shift</div>
+ <div class='action'>Hold for temporary select</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>space</div>
+ <div class='action'>Hold for temporary pan</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>/</div>
+ <div class='action'>Search</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>enter</div>
+ <div class='action'>Step through search results</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>f</div>
+ <div class='action'>Zoom into selection</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>z/0</div>
+ <div class='action'>Reset zoom and pan</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>g/G</div>
+ <div class='action'>Toggle 60hz grid</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>v</div>
+ <div class='action'>Highlight VSync</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>h</div>
+ <div class='action'>Toggle low/high details</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>m</div>
+ <div class='action'>Mark current selection</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>p</div>
+ <div class='action'>Select power samples over current selection interval</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>`</div>
+ <div class='action'>Show or hide the scripting console</div>
+ </div>
+
+ <div class='pair'>
+ <div class='command'>?</div>
+ <div class='action'>Show help</div>
+ </div>
+ </div>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-timeline-view-help-overlay',
+
+ ready() {
+ const mod = tr.isMac ? 'cmd ' : 'ctrl';
+ const spans = Polymer.dom(this.root).querySelectorAll(
+ 'span.mod');
+ for (let i = 0; i < spans.length; i++) {
+ Polymer.dom(spans[i]).textContent = mod;
+ }
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html
new file mode 100644
index 00000000000..1b705eea28c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_help_overlay_test.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/timeline_view_help_overlay.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('inactive', function() {
+ const el = document.createElement('tr-ui-timeline-view-help-overlay');
+ this.addHTMLOutput(el);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html
new file mode 100644
index 00000000000..45f0efa1a2a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+<link rel="import" href="/tracing/ui/analysis/generic_object_view.html">
+<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+<link rel="import" href="/tracing/ui/base/table.html">
+
+<dom-module id='tr-ui-timeline-view-metadata-overlay'>
+ <template>
+ <style>
+ :host {
+ width: 700px;
+
+ overflow: auto;
+ }
+ </style>
+ <tr-ui-b-table id="table"></tr-ui-b-table>
+ </template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-timeline-view-metadata-overlay',
+
+ created() {
+ this.metadata_ = undefined;
+ },
+
+ ready() {
+ this.$.table.tableColumns = [
+ {
+ title: 'name',
+ value: d => d.name,
+ },
+ {
+ title: 'value',
+ value: d => {
+ const gov = document.createElement('tr-ui-a-generic-object-view');
+ gov.object = d.value;
+ return gov;
+ },
+ }
+ ];
+ },
+
+ get metadata() {
+ return this.metadata_;
+ },
+
+ set metadata(metadata) {
+ this.metadata_ = metadata;
+ this.$.table.tableRows = this.metadata_;
+ this.$.table.rebuild();
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html
new file mode 100644
index 00000000000..82f974ac7e9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_metadata_overlay_test.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/tracing/ui/timeline_view_metadata_overlay.html">
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('inactive', function() {
+ const el = document.createElement('tr-ui-timeline-view-metadata-overlay');
+ el.metadata = [
+ {
+ name: 'clientInfo',
+ value: {
+ command_line: './out/Release/Chromium.app/Contents/MacOS/Chromium --enable-threaded-compositing --force-compositing-mode --enable-impl-side-painting --enable-skia-benchmarking --allow-webui-compositing --flag-switches-begin --force-compositing-mode --disable-threaded-compositing --flag-switches-end', // @suppress longLineCheck
+ version: 'Chrome/29.0.1521.0'
+ }
+ },
+ {
+ name: 'somethingElse',
+ value: 'fascinating!'
+ }
+ ];
+
+ this.addHTMLOutput(el);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html
new file mode 100644
index 00000000000..7591f82df77
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_view_test.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/timeline_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Task = tr.b.Task;
+
+ function setupTimeline() {
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+
+ const view = document.createElement('tr-ui-timeline-view');
+ Polymer.dom(view).appendChild(container);
+ view.trackViewContainer_ = container;
+ return view;
+ }
+
+ const createFullyPopulatedModel = function(opt_withError, opt_withMetadata) {
+ const withError = opt_withError !== undefined ? opt_withError : true;
+ const withMetadata = opt_withMetadata !== undefined ?
+ opt_withMetadata : true;
+
+ const numTests = 50;
+ let testIndex = 0;
+ const startTime = 0;
+
+ const model = new tr.Model();
+ const io = new tr.importer.ImportOptions();
+ model.importOptions = io;
+
+ const cpu = model.kernel.getOrCreateCpu(0);
+ cpu.getOrCreateCounter('Category Name', 'Counter Name Here');
+ cpu.createSubSlices();
+
+ for (testIndex = 0; testIndex < numTests; ++testIndex) {
+ const process = model.getOrCreateProcess(10000 + testIndex);
+ if (testIndex % 2 === 0) {
+ const thread = process.getOrCreateThread('Thread Name Here');
+ thread.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ 'foo', 'a', 0, startTime, {}, 1));
+ thread.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ 'bar', 'b', 0, startTime + 23, {}, 10));
+ } else {
+ const thread = process.getOrCreateThread('Name');
+ thread.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ 'foo', 'a', 0, startTime + 4, {}, 11));
+ thread.sliceGroup.pushSlice(new tr.model.ThreadSlice(
+ 'bar', 'b', 0, startTime + 22, {}, 14));
+ }
+ }
+ const p1000 = model.getOrCreateProcess(1000);
+ const objects = p1000.objects;
+ objects.idWasCreated('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10);
+ objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 10,
+ 'snapshot-1');
+ objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 25,
+ 'snapshot-2');
+ objects.addSnapshot('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 40,
+ 'snapshot-3');
+ objects.idWasDeleted('0x1000', 'tr.e.cc', 'LayerTreeHostImpl', 45);
+ model.updateCategories_();
+
+ // Add a known problematic piece of data to test the import errors UI.
+ model.importWarning({
+ type: 'test_error',
+ message: 'Synthetic Import Error',
+ showToUser: true,
+ });
+ model.updateBounds();
+
+ // Add data with metadata information stored
+ model.metadata.push({name: 'a', value: 'testA'});
+ model.metadata.push({name: 'b', value: 'testB'});
+ model.metadata.push({name: 'c', value: 'testC'});
+
+ return model;
+ };
+
+ const visibleTracks = function(trackButtons) {
+ return trackButtons.reduce(function(numVisible, button) {
+ const style = button.parentElement.style;
+ const visible = (style.display.indexOf('none') === -1);
+ return visible ? numVisible + 1 : numVisible;
+ }, 0);
+ };
+
+ const modelsEquivalent = function(lhs, rhs) {
+ if (lhs.length !== rhs.length) return false;
+
+ return lhs.every(function(lhsItem, index) {
+ const rhsItem = rhs[index];
+ return rhsItem.regexpText === lhsItem.regexpText &&
+ rhsItem.isOn === lhsItem.isOn;
+ });
+ };
+
+ test('instantiate', async function() {
+ const model11 = createFullyPopulatedModel(true, true);
+
+ const view = setupTimeline();
+ view.style.height = '400px';
+ view.style.border = '1px solid black';
+ view.model = model11;
+
+ const simpleButton1 = document.createElement('tr-ui-b-toolbar-button');
+ Polymer.dom(simpleButton1).textContent = 'M';
+ Polymer.dom(view.leftControls).appendChild(simpleButton1);
+
+ const simpleButton2 = document.createElement('tr-ui-b-toolbar-button');
+ Polymer.dom(simpleButton2).textContent = 'am button';
+ Polymer.dom(view.leftControls).appendChild(simpleButton2);
+
+ this.addHTMLOutput(view);
+ await view.builtPromise;
+ });
+
+ test('changeModelToSomethingDifferent', async function() {
+ const model00 = createFullyPopulatedModel(false, false);
+ const model11 = createFullyPopulatedModel(true, true);
+
+ const view = setupTimeline();
+ view.style.height = '400px';
+ view.model = model00;
+ view.model = undefined;
+ view.model = model11;
+ view.model = model00;
+
+ this.addHTMLOutput(view);
+ await view.builtPromise;
+ });
+
+ test('setModelToSameThingAgain', async function() {
+ const model = createFullyPopulatedModel(false, false);
+
+ // Create a view with a model.
+ const view = setupTimeline();
+ this.addHTMLOutput(view);
+ view.style.height = '400px';
+ view.model = model;
+ const sc = view.brushingStateController;
+
+ // Mutate the model and update the view.
+ const t123 = model.getOrCreateProcess(123).getOrCreateThread(123);
+ t123.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'somethingUnusual', start: 0, duration: 5}));
+ view.model = model;
+
+ await view.builtPromise;
+
+ // Verify that the new bits of the model show up in the view.
+ const selection = new tr.model.EventSet();
+ const filter = new tr.c.TitleOrCategoryFilter('somethingUnusual');
+ const filterTask = sc.addAllEventsMatchingFilterToSelectionAsTask(
+ filter, selection);
+ Task.RunSynchronously(filterTask);
+ assert.strictEqual(selection.length, 1);
+ });
+
+ test('setModelBeforeAttached', async function() {
+ const view = document.createElement('tr-ui-timeline-view');
+ view.style.height = '400px';
+ view.model = createFullyPopulatedModel(false, false);
+
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+ Polymer.dom(view).appendChild(container);
+ this.addHTMLOutput(view);
+ await view.builtPromise;
+ });
+
+ test('filterProcessesUI', async function() {
+ const view = document.createElement('tr-ui-timeline-view');
+ view.style.height = '400px';
+ view.model = createFullyPopulatedModel(false, false);
+
+ const container = document.createElement('track-view-container');
+ container.id = 'track_view_container';
+ Polymer.dom(view).appendChild(container);
+ this.addHTMLOutput(view);
+ await view.builtPromise;
+
+ const procFilter = Polymer.dom(view.processFilter_);
+ const checkboxes = procFilter.querySelectorAll('input[type=checkbox]');
+ assert.lengthOf(checkboxes, 52);
+
+ const trackView =
+ view.trackViewContainer_.querySelector('tr-ui-timeline-track-view');
+ const countVisibleTracks = () => trackView.processViews.filter(
+ view => view.visible).length;
+ assert.strictEqual(checkboxes.length, trackView.processViews.length);
+ assert.strictEqual(countVisibleTracks(), 52);
+ assert.isTrue(trackView.processViews[0].visible);
+
+ checkboxes[0].click();
+ assert.strictEqual(countVisibleTracks(), 51);
+ assert.isFalse(trackView.processViews[0].visible);
+ assert.isTrue(trackView.processViews[1].visible);
+
+ // Hide a track. Validate that the checkbox updated state correctly.
+ trackView.processViews[1].visible = false;
+ assert.isFalse(trackView.processViews[1].visible);
+ assert.isFalse(checkboxes[1].checked);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html
new file mode 100644
index 00000000000..58dafb55aff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport.html
@@ -0,0 +1,442 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/event.html">
+<link rel="import" href="/tracing/base/unit.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/animation.html">
+<link rel="import" href="/tracing/ui/base/animation_controller.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/timeline_interest_range.html">
+<link rel="import" href="/tracing/ui/tracks/container_to_track_map.html">
+<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Code for the viewport.
+ */
+tr.exportTo('tr.ui', function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const TimelineInterestRange = tr.ui.TimelineInterestRange;
+
+ const IDEAL_MAJOR_MARK_DISTANCE_PX = 150;
+ // Keep 5 digits of precision when rounding the major mark distances.
+ const MAJOR_MARK_ROUNDING_FACTOR = 100000;
+
+ class AnimationControllerProxy {
+ constructor(target) {
+ this.target_ = target;
+ }
+
+ get panX() {
+ return this.target_.currentDisplayTransform_.panX;
+ }
+
+ set panX(panX) {
+ this.target_.currentDisplayTransform_.panX = panX;
+ }
+
+ get panY() {
+ return this.target_.currentDisplayTransform_.panY;
+ }
+
+ set panY(panY) {
+ this.target_.currentDisplayTransform_.panY = panY;
+ }
+
+ get scaleX() {
+ return this.target_.currentDisplayTransform_.scaleX;
+ }
+
+ set scaleX(scaleX) {
+ this.target_.currentDisplayTransform_.scaleX = scaleX;
+ }
+
+ cloneAnimationState() {
+ return this.target_.currentDisplayTransform_.clone();
+ }
+
+ xPanWorldPosToViewPos(xWorld, xView) {
+ this.target_.currentDisplayTransform_.xPanWorldPosToViewPos(
+ xWorld, xView, this.target_.modelTrackContainer_.canvas.clientWidth);
+ }
+ }
+
+ /**
+ * The TimelineViewport manages the transform used for navigating
+ * within the timeline. It is a simple transform:
+ * x' = (x+pan) * scale
+ *
+ * The timeline code tries to avoid directly accessing this transform,
+ * instead using this class to do conversion between world and viewspace,
+ * as well as the math for centering the viewport in various interesting
+ * ways.
+ *
+ * @constructor
+ * @extends {tr.b.EventTarget}
+ */
+ function TimelineViewport(parentEl) {
+ this.parentEl_ = parentEl;
+ this.modelTrackContainer_ = undefined;
+ this.currentDisplayTransform_ = new TimelineDisplayTransform();
+ this.initAnimationController_();
+
+ // Flow events
+ this.showFlowEvents_ = false;
+
+ // Highlights.
+ this.highlightVSync_ = false;
+
+ // High details.
+ this.highDetails_ = false;
+
+ // Grid system.
+ this.gridTimebase_ = 0;
+ this.gridStep_ = 1000 / 60;
+ this.gridEnabled_ = false;
+
+ // Init logic.
+ this.hasCalledSetupFunction_ = false;
+
+ this.onResize_ = this.onResize_.bind(this);
+ this.onModelTrackControllerScroll_ =
+ this.onModelTrackControllerScroll_.bind(this);
+
+ this.timeMode_ = TimelineViewport.TimeMode.TIME_IN_MS;
+ // Major mark positions are where the gridlines/ruler marks are placed along
+ // the x-axis.
+ this.majorMarkWorldPositions_ = [];
+ this.majorMarkUnit_ = undefined;
+ this.interestRange_ = new TimelineInterestRange(this);
+
+ this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
+ this.containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
+
+ this.dispatchChangeEvent = this.dispatchChangeEvent.bind(this);
+ }
+
+ TimelineViewport.TimeMode = {
+ TIME_IN_MS: 0,
+ REVISIONS: 1
+ };
+
+ TimelineViewport.prototype = {
+ __proto__: tr.b.EventTarget.prototype,
+
+ /**
+ * @return {boolean} Whether the current timeline is attached to the
+ * document.
+ */
+ get isAttachedToDocumentOrInTestMode() {
+ // Allow not providing a parent element, used by tests.
+ if (this.parentEl_ === undefined) return;
+ return tr.ui.b.isElementAttachedToDocument(this.parentEl_);
+ },
+
+ onResize_() {
+ this.dispatchChangeEvent();
+ },
+
+ /**
+ * Fires the change event on this viewport. Used to notify listeners
+ * to redraw when the underlying model has been mutated.
+ */
+ dispatchChangeEvent() {
+ tr.b.dispatchSimpleEvent(this, 'change');
+ },
+
+ detach() {
+ window.removeEventListener('resize', this.dispatchChangeEvent);
+ },
+
+ initAnimationController_() {
+ this.dtAnimationController_ = new tr.ui.b.AnimationController();
+ this.dtAnimationController_.addEventListener(
+ 'didtick', function(e) {
+ this.onCurentDisplayTransformChange_(e.oldTargetState);
+ }.bind(this));
+
+ this.dtAnimationController_.target = new AnimationControllerProxy(this);
+ },
+
+ get currentDisplayTransform() {
+ return this.currentDisplayTransform_;
+ },
+
+ setDisplayTransformImmediately(displayTransform) {
+ this.dtAnimationController_.cancelActiveAnimation();
+
+ const oldDisplayTransform =
+ this.dtAnimationController_.target.cloneAnimationState();
+ this.currentDisplayTransform_.set(displayTransform);
+ this.onCurentDisplayTransformChange_(oldDisplayTransform);
+ },
+
+ queueDisplayTransformAnimation(animation) {
+ if (!(animation instanceof tr.ui.b.Animation)) {
+ throw new Error('animation must be instanceof tr.ui.b.Animation');
+ }
+ this.dtAnimationController_.queueAnimation(animation);
+ },
+
+ onCurentDisplayTransformChange_(oldDisplayTransform) {
+ // Ensure panY stays clamped in the track container's scroll range.
+ if (this.modelTrackContainer_) {
+ this.currentDisplayTransform.panY = tr.b.math.clamp(
+ this.currentDisplayTransform.panY,
+ 0,
+ this.modelTrackContainer_.scrollHeight -
+ this.modelTrackContainer_.clientHeight);
+ }
+
+ const changed = !this.currentDisplayTransform.equals(oldDisplayTransform);
+ const yChanged = this.currentDisplayTransform.panY !==
+ oldDisplayTransform.panY;
+ if (yChanged) {
+ this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY;
+ }
+ if (changed) {
+ this.dispatchChangeEvent();
+ }
+ },
+
+ onModelTrackControllerScroll_(e) {
+ if (this.dtAnimationController_.activeAnimation &&
+ this.dtAnimationController_.activeAnimation.affectsPanY) {
+ this.dtAnimationController_.cancelActiveAnimation();
+ }
+ const panY = this.modelTrackContainer_.scrollTop;
+ this.currentDisplayTransform_.panY = panY;
+ },
+
+ get modelTrackContainer() {
+ return this.modelTrackContainer_;
+ },
+
+ set modelTrackContainer(m) {
+ if (this.modelTrackContainer_) {
+ this.modelTrackContainer_.removeEventListener('scroll',
+ this.onModelTrackControllerScroll_);
+ }
+
+ this.modelTrackContainer_ = m;
+ this.modelTrackContainer_.addEventListener('scroll',
+ this.onModelTrackControllerScroll_);
+ },
+
+ get showFlowEvents() {
+ return this.showFlowEvents_;
+ },
+
+ set showFlowEvents(showFlowEvents) {
+ this.showFlowEvents_ = showFlowEvents;
+ this.dispatchChangeEvent();
+ },
+
+ get highlightVSync() {
+ return this.highlightVSync_;
+ },
+
+ set highlightVSync(highlightVSync) {
+ this.highlightVSync_ = highlightVSync;
+ this.dispatchChangeEvent();
+ },
+
+ get highDetails() {
+ return this.highDetails_;
+ },
+
+ set highDetails(highDetails) {
+ this.highDetails_ = highDetails;
+ this.dispatchChangeEvent();
+ },
+
+ get gridEnabled() {
+ return this.gridEnabled_;
+ },
+
+ set gridEnabled(enabled) {
+ if (this.gridEnabled_ === enabled) return;
+
+ this.gridEnabled_ = enabled && true;
+ this.dispatchChangeEvent();
+ },
+
+ get gridTimebase() {
+ return this.gridTimebase_;
+ },
+
+ set gridTimebase(timebase) {
+ if (this.gridTimebase_ === timebase) return;
+
+ this.gridTimebase_ = timebase;
+ this.dispatchChangeEvent();
+ },
+
+ get gridStep() {
+ return this.gridStep_;
+ },
+
+ get interestRange() {
+ return this.interestRange_;
+ },
+
+ get majorMarkWorldPositions() {
+ return this.majorMarkWorldPositions_;
+ },
+
+ get majorMarkUnit() {
+ switch (this.timeMode_) {
+ case TimelineViewport.TimeMode.TIME_IN_MS:
+ return tr.b.Unit.byName.timeInMsAutoFormat;
+ case TimelineViewport.TimeMode.REVISIONS:
+ return tr.b.Unit.byName.count;
+ default:
+ throw new Error(
+ 'Cannot get Unit for unsupported time mode ' + this.timeMode_);
+ }
+ },
+
+ get timeMode() {
+ return this.timeMode_;
+ },
+
+ set timeMode(mode) {
+ this.timeMode_ = mode;
+ this.dispatchChangeEvent();
+ },
+
+ updateMajorMarkData(viewLWorld, viewRWorld) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const dt = this.currentDisplayTransform;
+
+ const idealMajorMarkDistancePix =
+ IDEAL_MAJOR_MARK_DISTANCE_PX * pixelRatio;
+ const idealMajorMarkDistanceWorld =
+ dt.xViewVectorToWorld(idealMajorMarkDistancePix);
+
+ const majorMarkDistanceWorld = tr.b.math.preferredNumberLargerThanMin(
+ idealMajorMarkDistanceWorld);
+
+ const firstMajorMark = Math.floor(
+ viewLWorld / majorMarkDistanceWorld) * majorMarkDistanceWorld;
+
+ this.majorMarkWorldPositions_ = [];
+ for (let curX = firstMajorMark;
+ curX < viewRWorld;
+ curX += majorMarkDistanceWorld) {
+ this.majorMarkWorldPositions_.push(
+ Math.floor(MAJOR_MARK_ROUNDING_FACTOR * curX) /
+ MAJOR_MARK_ROUNDING_FACTOR);
+ }
+ },
+
+ drawMajorMarkLines(ctx, viewHeight) {
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ ctx.save();
+ ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+ ctx.beginPath();
+ for (const majorMark of this.majorMarkWorldPositions_) {
+ const x = this.currentDisplayTransform.xWorldToView(majorMark);
+ tr.ui.b.drawLine(ctx, x, 0, x, viewHeight);
+ }
+ ctx.strokeStyle = '#ddd';
+ ctx.stroke();
+
+ ctx.restore();
+ },
+
+ drawGridLines(ctx, viewLWorld, viewRWorld, viewHeight) {
+ if (!this.gridEnabled) return;
+
+ const dt = this.currentDisplayTransform;
+ let x = this.gridTimebase;
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ ctx.save();
+ ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+ ctx.beginPath();
+ while (x < viewRWorld) {
+ if (x >= viewLWorld) {
+ // Do conversion to viewspace here rather than on
+ // x to avoid precision issues.
+ const vx = Math.floor(dt.xWorldToView(x));
+ tr.ui.b.drawLine(ctx, vx, 0, vx, viewHeight);
+ }
+
+ x += this.gridStep;
+ }
+ ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)';
+ ctx.stroke();
+
+ ctx.restore();
+ },
+
+ /**
+ * Helper for selection previous or next.
+ * @param {boolean} offset If positive, select one forward (next).
+ * Else, select previous.
+ *
+ * @return {boolean} true if current selection changed.
+ */
+ getShiftedSelection(selection, offset) {
+ const newSelection = new tr.model.EventSet();
+ for (const event of selection) {
+ // If this is a flow event, then move to its slice based on the
+ // offset direction.
+ if (event instanceof tr.model.FlowEvent) {
+ if (offset > 0) {
+ newSelection.push(event.endSlice);
+ } else if (offset < 0) {
+ newSelection.push(event.startSlice);
+ } else {
+ /* Do nothing. Zero offsets don't do anything. */
+ }
+ continue;
+ }
+
+ const track = this.trackForEvent(event);
+ track.addEventNearToProvidedEventToSelection(
+ event, offset, newSelection);
+ }
+
+ if (newSelection.length === 0) return undefined;
+
+ return newSelection;
+ },
+
+ rebuildEventToTrackMap() {
+ // TODO(charliea): Make the event to track map have a similar interface
+ // to the container to track map so that we can just clear() here.
+ this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
+ this.modelTrackContainer_.addEventsToTrackMap(this.eventToTrackMap_);
+ },
+
+ rebuildContainerToTrackMap() {
+ this.containerToTrackMap.clear();
+ this.modelTrackContainer_.addContainersToTrackMap(
+ this.containerToTrackMap);
+ },
+
+ trackForEvent(event) {
+ return this.eventToTrackMap_[event.guid];
+ }
+ };
+
+ return {
+ TimelineViewport,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html
new file mode 100644
index 00000000000..5715182e720
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/timeline_viewport_test.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/location.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/base/constants.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Location = tr.model.Location;
+ const Model = tr.Model;
+
+ test('memoization', function() {
+ const vp = new tr.ui.TimelineViewport(document.createElement('div'));
+
+ const slice = { guid: 1 };
+
+ vp.modelTrackContainer = {
+ addEventsToTrackMap(eventToTrackMap) {
+ eventToTrackMap.addEvent(slice, 'track');
+ },
+ addEventListener() {}
+ };
+
+ assert.isUndefined(vp.trackForEvent(slice));
+ vp.rebuildEventToTrackMap();
+
+ assert.strictEqual(vp.trackForEvent(slice), 'track');
+ });
+
+ test('shiftedSelection', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3));
+ t1.sliceGroup.pushSlice(
+ new tr.model.ThreadSlice('', 'a', 0, 5, {}, 1));
+
+ const viewport = new tr.ui.TimelineViewport();
+ const track = new tr.ui.tracks.SliceTrack(viewport);
+ viewport.modelTrackContainer = track;
+ track.slices = t1.sliceGroup.slices;
+
+ viewport.rebuildEventToTrackMap();
+
+ const sel = new tr.model.EventSet();
+ sel.push(t1.sliceGroup.slices[0]);
+
+ const shifted = track.viewport.getShiftedSelection(sel, 1);
+ assert.isTrue(shifted.equals(
+ new tr.model.EventSet(t1.sliceGroup.slices[1])));
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html
new file mode 100644
index 00000000000..571b0543bb9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of alert objects.
+ * @constructor
+ * @extends {LetterDotTrack}
+ */
+ const AlertTrack = tr.ui.b.define(
+ 'alert-track', tr.ui.tracks.LetterDotTrack);
+
+ AlertTrack.prototype = {
+ __proto__: tr.ui.tracks.LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Alerts';
+ this.alerts_ = undefined;
+ },
+
+ get alerts() {
+ return this.alerts_;
+ },
+
+ set alerts(alerts) {
+ this.alerts_ = alerts;
+ if (alerts === undefined) {
+ this.items = undefined;
+ return;
+ }
+ this.items = this.alerts_.map(function(alert) {
+ return new tr.ui.tracks.LetterDot(
+ alert, String.fromCharCode(9888), alert.colorId, alert.start);
+ });
+ }
+ };
+
+ return {
+ AlertTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html
new file mode 100644
index 00000000000..4e60180b00e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/alert_track_test.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AlertTrack = tr.ui.tracks.AlertTrack;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const ALERT_INFO_1 = new tr.model.EventInfo(
+ 'Alert 1', 'One alert');
+ const ALERT_INFO_2 = new tr.model.EventInfo(
+ 'Alert 2', 'Another alert');
+
+ const createAlerts = function() {
+ const alerts = [
+ new tr.model.Alert(ALERT_INFO_1, 5),
+ new tr.model.Alert(ALERT_INFO_1, 20),
+ new tr.model.Alert(ALERT_INFO_2, 35),
+ new tr.model.Alert(ALERT_INFO_2, 50)
+ ];
+ return alerts;
+ };
+
+ test('instantiate', function() {
+ const alerts = createAlerts();
+ alerts[1].selectionState = SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = AlertTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.alerts = alerts;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+
+ assert.strictEqual(5, track.items[0].start);
+ });
+
+ test('modelMapping', function() {
+ const alerts = createAlerts();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = AlertTrack(viewport);
+ track.alerts = alerts;
+
+ const a0 = track.items[0].modelItem;
+ assert.strictEqual(a0, alerts[0]);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html
new file mode 100644
index 00000000000..d922030ce70
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a AsyncSliceGroup.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const AsyncSliceGroupTrack = tr.ui.b.define(
+ 'async-slice-group-track',
+ tr.ui.tracks.MultiRowTrack);
+
+ AsyncSliceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('async-slice-group-track');
+ this.group_ = undefined;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ track.asyncStyle = true;
+ return track;
+ },
+
+ get group() {
+ return this.group_;
+ },
+
+ set group(group) {
+ this.group_ = group;
+ this.buildAndSetSubRows_();
+ },
+
+ get eventContainer() {
+ return this.group;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.MultiRowTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.group, this);
+ },
+
+ buildAndSetSubRows_() {
+ if (this.group_.viewSubGroups.length <= 1) {
+ // No nested groups or just only one, the most common case.
+ const rows = groupAsyncSlicesIntoSubRows(this.group_.slices);
+ const rowsWithHeadings = rows.map(row => {
+ return {row, heading: undefined};
+ });
+ this.setPrebuiltSubRows(this.group_, rowsWithHeadings);
+ return;
+ }
+
+ // We have nested grouping level (no further levels supported),
+ // so process sub-groups separately and preserve their titles.
+ const rowsWithHeadings = [];
+ for (const subGroup of this.group_.viewSubGroups) {
+ const subGroupRows = groupAsyncSlicesIntoSubRows(subGroup.slices);
+ if (subGroupRows.length === 0) {
+ continue;
+ }
+ for (let i = 0; i < subGroupRows.length; i++) {
+ rowsWithHeadings.push({
+ row: subGroupRows[i],
+ heading: (i === 0 ? subGroup.title : '')
+ });
+ }
+ }
+ this.setPrebuiltSubRows(this.group_, rowsWithHeadings);
+ }
+ };
+
+ /**
+ * Strip away wrapper slice which are used to group slices into
+ * a single track but provide no information themselves.
+ */
+ function stripSlice_(slice) {
+ if (slice.subSlices !== undefined && slice.subSlices.length === 1) {
+ const subSlice = slice.subSlices[0];
+ if (tr.b.math.approximately(subSlice.start, slice.start, 1) &&
+ tr.b.math.approximately(subSlice.duration, slice.duration, 1)) {
+ return subSlice;
+ }
+ }
+ return slice;
+ }
+
+ /**
+ * Unwrap the list of non-overlapping slices into a number of rows where
+ * the top row holds original slices and additional rows hold nested slices
+ * of ones from the row above them.
+ */
+ function makeLevelSubRows_(slices) {
+ const rows = [];
+ const putSlice = (slice, level) => {
+ while (rows.length <= level) {
+ rows.push([]);
+ }
+ rows[level].push(slice);
+ };
+ const putSliceRecursively = (slice, level) => {
+ putSlice(slice, level);
+ if (slice.subSlices !== undefined) {
+ for (const subSlice of slice.subSlices) {
+ putSliceRecursively(subSlice, level + 1);
+ }
+ }
+ };
+
+ for (const slice of slices) {
+ putSliceRecursively(stripSlice_(slice), 0);
+ }
+ return rows;
+ }
+
+ /**
+ * Breaks up the list of slices into a number of rows:
+ * - Which contain non-overlapping slices.
+ * - If slice has nested slices, they're placed onto the row below.
+ * Sorting may be skipped if slices are already sorted by start timestamp.
+ */
+ function groupAsyncSlicesIntoSubRows(slices, opt_skipSort) {
+ if (!opt_skipSort) {
+ slices.sort((x, y) => x.start - y.start);
+ }
+
+ // The algorithm is fairly simple:
+ // - Level is a group of rows, where the top row holds original slices and
+ // additional rows hold nested slices of ones from the row above them.
+ // - Make a level by putting sorted slices, skipping if one's overlapping.
+ // - Repeat and make more levels while we're having residual slices left.
+ const rows = [];
+ let slicesLeft = slices;
+ while (slicesLeft.length !== 0) {
+ // Make a level.
+ const fit = [];
+ const unfit = [];
+ let levelEndTime = -1;
+
+ for (const slice of slicesLeft) {
+ if (slice.start >= levelEndTime) {
+ // Assuming nested slices lie within parent's boundaries.
+ levelEndTime = slice.end;
+ fit.push(slice);
+ } else {
+ unfit.push(slice);
+ }
+ }
+ rows.push(...makeLevelSubRows_(fit));
+ slicesLeft = unfit;
+ }
+ return rows;
+ }
+
+ return {
+ AsyncSliceGroupTrack,
+ groupAsyncSlicesIntoSubRows,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html
new file mode 100644
index 00000000000..96003e1b5f2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/async_slice_group_track_test.html
@@ -0,0 +1,328 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const AsyncSliceGroup = tr.model.AsyncSliceGroup;
+ const AsyncSliceGroupTrack = tr.ui.tracks.AsyncSliceGroupTrack;
+ const Process = tr.model.Process;
+ const ProcessTrack = tr.ui.tracks.ProcessTrack;
+ const Thread = tr.model.Thread;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed;
+ const groupAsyncSlicesIntoSubRows = tr.ui.tracks.groupAsyncSlicesIntoSubRows;
+
+ test('filterSubRows', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ assert.strictEqual(track.children.length, 1);
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_empty', function() {
+ const rows = groupAsyncSlicesIntoSubRows([]);
+ assert.strictEqual(rows.length, 0);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_trivial', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+
+ const s1 = newAsyncSlice(10, 200, t1, t1);
+ const s2 = newAsyncSlice(300, 30, t1, t1);
+
+ const slices = [s2, s1];
+ const rows = groupAsyncSlicesIntoSubRows(slices);
+
+ assert.strictEqual(rows.length, 1);
+ assert.sameMembers(rows[0], [s1, s2]);
+ });
+
+ test('groupAsyncSlicesIntoSubRows_nonTrivial', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+
+ const s1 = newAsyncSlice(10, 200, t1, t1); // Should be stripped.
+ const s1s1 = newAsyncSlice(10, 200, t1, t1);
+ s1.subSlices = [s1s1];
+
+ const s2 = newAsyncSlice(300, 30, t1, t1);
+ const s2s1 = newAsyncSlice(300, 10, t1, t1);
+ const s2s2 = newAsyncSlice(310, 20, t1, t1); // Should not be stripped.
+ const s2s2s1 = newAsyncSlice(310, 20, t1, t1);
+ s2s2.subSlices = [s2s2s1];
+ s2.subSlices = [s2s2, s2s1];
+
+ const s3 = newAsyncSlice(200, 50, t1, t1); // Overlaps with s1.
+ const s3s1 = newAsyncSlice(220, 5, t1, t1);
+ s3.subSlices = [s3s1];
+
+ const slices = [s2, s3, s1];
+ const rows = groupAsyncSlicesIntoSubRows(slices);
+
+ assert.strictEqual(rows.length, 5);
+ assert.sameMembers(rows[0], [s1s1, s2]);
+ assert.sameMembers(rows[1], [s2s1, s2s2]);
+ assert.sameMembers(rows[2], [s2s2s1]);
+ assert.sameMembers(rows[3], [s3]);
+ assert.sameMembers(rows[4], [s3s1]);
+ });
+
+ test('rebuildSubRows_twoNonOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ const s1 = newAsyncSlice(0, 1, t1, t1);
+ const subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s1.subSlices = [subs1];
+ g.push(s1);
+ g.push(newAsyncSlice(1, 1, t1, t1));
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ const subRows = track.subRows;
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.sameMembers(g.slices[1].subSlices, []);
+ });
+
+ test('rebuildSubRows_twoOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const s1 = newAsyncSlice(0, 1, t1, t1);
+ const subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s1.subSlices = [subs1];
+ const s2 = newAsyncSlice(0, 1.5, t1, t1);
+ const subs2 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+ s2.subSlices = [subs2];
+ g.push(s1);
+ g.push(s2);
+
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.strictEqual(subRows[1][0], g.slices[1].subSlices[0]);
+ });
+
+ test('rebuildSubRows_threePartlyOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ g.push(newAsyncSlice(0, 1.5, t1, t1));
+ g.push(newAsyncSlice(1, 1.5, t1, t1));
+ g.updateBounds();
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ const subRows = track.subRows;
+
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.strictEqual(subRows[0][0], g.slices[0]);
+ assert.strictEqual(subRows[0][1], g.slices[2]);
+ assert.strictEqual(subRows[1][0], g.slices[1]);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.sameMembers(g.slices[0].subSlices, []);
+ assert.sameMembers(g.slices[1].subSlices, []);
+ assert.sameMembers(g.slices[2].subSlices, []);
+ });
+
+ test('rebuildSubRows_threeOverlappingSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ g.push(newAsyncSlice(0, 1, t1, t1));
+ g.push(newAsyncSlice(0, 1.5, t1, t1));
+ g.push(newAsyncSlice(2, 1, t1, t1));
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.strictEqual(subRows[0][0], g.slices[0]);
+ assert.strictEqual(subRows[1][0], g.slices[1]);
+ assert.strictEqual(subRows[0][1], g.slices[2]);
+ });
+
+ test('rebuildSubRows_twoViewSubGroups', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+ g.push(newAsyncSliceNamed('foo', 0, 1, t1, t1));
+ g.push(newAsyncSliceNamed('foo', 2, 1, t1, t1));
+ g.push(newAsyncSliceNamed('bar', 1, 2, t1, t1));
+ g.push(newAsyncSliceNamed('bar', 3, 2, t1, t1));
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+ track.heading = 'sup';
+
+ assert.strictEqual(track.subRows.length, 2);
+ const subTracks = Polymer.dom(track).children;
+ assert.strictEqual(subTracks.length, 3);
+ assert.strictEqual(subTracks[0].slices.length, 0);
+ assert.strictEqual(subTracks[1].slices.length, 2);
+ assert.strictEqual(subTracks[2].slices.length, 2);
+ const headings =
+ [subTracks[0].heading, subTracks[1].heading, subTracks[2].heading];
+ assert.sameMembers(headings, ['foo', 'bar', 'sup']);
+ });
+
+ // Tests that no slices and their sub slices overlap.
+ test('rebuildSubRows_NonOverlappingSubSlices', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const slice1 = newAsyncSlice(0, 5, t1, t1);
+ const slice1Child = newAsyncSlice(1, 2, t1, t1);
+ slice1.subSlices = [slice1Child];
+ const slice2 = newAsyncSlice(3, 5, t1, t1);
+ const slice3 = newAsyncSlice(5, 4, t1, t1);
+ const slice3Child = newAsyncSlice(6, 2, t1, t1);
+ slice3.subSlices = [slice3Child];
+ g.push(slice1);
+ g.push(slice2);
+ g.push(slice3);
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ // Checks each sub row to see that we don't have any overlapping slices.
+ for (let i = 0; i < subRows.length; i++) {
+ const row = subRows[i];
+ for (let j = 0; j < row.length; j++) {
+ for (let k = j + 1; k < row.length; k++) {
+ assert.isTrue(row[j].end <= row[k].start);
+ }
+ }
+ }
+ });
+
+ test('rebuildSubRows_NonOverlappingSubSlicesThreeNestedLevels', function() {
+ const model = new tr.Model();
+ const p1 = new Process(model, 1);
+ const t1 = new Thread(p1, 1);
+ const g = new AsyncSliceGroup(t1);
+
+ const slice1 = newAsyncSlice(0, 4, t1, t1);
+ const slice1Child = newAsyncSlice(1, 2, t1, t1);
+ slice1.subSlices = [slice1Child];
+ const slice2 = newAsyncSlice(2, 7, t1, t1);
+ const slice3 = newAsyncSlice(5, 5, t1, t1);
+ const slice3Child = newAsyncSlice(6, 3, t1, t1);
+ const slice3Child2 = newAsyncSlice(7, 1, t1, t1);
+ slice3.subSlices = [slice3Child];
+ slice3Child.subSlices = [slice3Child2];
+ g.push(slice1);
+ g.push(slice2);
+ g.push(slice3);
+ g.updateBounds();
+
+ const track = new AsyncSliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = g;
+
+ const subRows = track.subRows;
+ // Checks each sub row to see that we don't have any overlapping slices.
+ for (let i = 0; i < subRows.length; i++) {
+ const row = subRows[i];
+ for (let j = 0; j < row.length; j++) {
+ for (let k = j + 1; k < row.length; k++) {
+ assert.isTrue(row[j].end <= row[k].start);
+ }
+ }
+ }
+ });
+
+ test('asyncSliceGroupContainerMap', function() {
+ const vp = new tr.ui.TimelineViewport();
+ const containerToTrack = vp.containerToTrackMap;
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(123);
+ const thread = process.getOrCreateThread(456);
+ const group = new AsyncSliceGroup(thread);
+
+ const processTrack = new ProcessTrack(vp);
+ const threadTrack = new ThreadTrack(vp);
+ const groupTrack = new AsyncSliceGroupTrack(vp);
+ processTrack.process = process;
+ threadTrack.thread = thread;
+ groupTrack.group = group;
+ Polymer.dom(processTrack).appendChild(threadTrack);
+ Polymer.dom(threadTrack).appendChild(groupTrack);
+
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+
+ assert.isUndefined(containerToTrack.getTrackByStableId('123'));
+ assert.isUndefined(containerToTrack.getTrackByStableId('123.456'));
+ assert.isUndefined(
+ containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'));
+
+ vp.modelTrackContainer = {
+ addContainersToTrackMap(containerToTrackMap) {
+ processTrack.addContainersToTrackMap(containerToTrackMap);
+ },
+ addEventListener() {}
+ };
+ vp.rebuildContainerToTrackMap();
+
+ // Check that all tracks call childs' addContainersToTrackMap()
+ // by checking the resulting map.
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123'), processTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456'), threadTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'),
+ groupTrack);
+
+ // Check the track's eventContainer getter.
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html
new file mode 100644
index 00000000000..1b73f367636
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A point in a chart series with x (timestamp) and y (value) coordinates
+ * and an associated model item. The point can optionally also have a base
+ * y coordinate (which for example corresponds to the bottom edge of the
+ * associated bar in a bar chart).
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function ChartPoint(modelItem, x, y, opt_yBase) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.x = x;
+ this.y = y;
+ this.dotLetter = undefined;
+
+ // If the base y-coordinate is undefined, the bottom edge of the associated
+ // bar in a bar chart will start at the outer bottom edge (which is most
+ // likely slightly below zero).
+ this.yBase = opt_yBase;
+ }
+
+ ChartPoint.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype,
+ };
+
+ return {
+ ChartPoint,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html
new file mode 100644
index 00000000000..e2d8bc3e11c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_point_test.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+
+ test('checkFields_withoutYBase', function() {
+ const event = {};
+ const point = new ChartPoint(event, 42, -7);
+
+ assert.strictEqual(point.modelItem, event);
+ assert.strictEqual(point.x, 42);
+ assert.strictEqual(point.y, -7);
+ assert.isUndefined(point.yBase);
+ });
+
+ test('checkFields_withYBase', function() {
+ const event = {};
+ const point = new ChartPoint(event, 111, 222, 333);
+
+ assert.strictEqual(point.modelItem, event);
+ assert.strictEqual(point.x, 111);
+ assert.strictEqual(point.y, 222);
+ assert.strictEqual(point.yBase, 333);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html
new file mode 100644
index 00000000000..45025d13e0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series.html
@@ -0,0 +1,566 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const EventPresenter = tr.ui.b.EventPresenter;
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * The type of a chart series.
+ * @enum
+ */
+ const ChartSeriesType = {
+ LINE: 0,
+ AREA: 1
+ };
+
+ // The default rendering configuration for ChartSeries.
+ const DEFAULT_RENDERING_CONFIG = {
+ // The type of the chart series.
+ chartType: ChartSeriesType.LINE,
+
+ // The size of a selected point dot in device-independent pixels (circle
+ // diameter).
+ selectedPointSize: 4,
+
+ // The size of an unselected point dot in device-independent pixels (square
+ // width/height).
+ unselectedPointSize: 3,
+
+ // Whether the selected dots should be solid circles of the line color, or
+ // filled with the background's selection color.
+ solidSelectedDots: false,
+
+ // The color of the chart.
+ colorId: 0,
+
+ // The width of the top line in device-independent pixels.
+ lineWidth: 1,
+
+ // Minimum distance between points in physical pixels. Points which are
+ // closer than this distance will be skipped.
+ skipDistance: 1,
+
+ // Density in points per physical pixel at which unselected point dots
+ // become transparent.
+ unselectedPointDensityTransparent: 0.10,
+
+ // Density in points per physical pixel at which unselected point dots
+ // become fully opaque.
+ unselectedPointDensityOpaque: 0.05,
+
+ // Opacity of area chart background.
+ backgroundOpacity: 0.5,
+
+ // Whether to graph steps between points. Set to false for lines instead.
+ stepGraph: true
+ };
+
+ // The virtual width of the last point in a series (whose rectangle has zero
+ // width) in world timestamps difference for the purposes of selection.
+ const LAST_POINT_WIDTH = 16;
+
+ // Constants for sizing and font of points with dot letters.
+ const DOT_LETTER_RADIUS_PX = 7;
+ const DOT_LETTER_RADIUS_PADDING_PX = 0.5;
+ const DOT_LETTER_SELECTED_OUTLINE_WIDTH_PX = 3;
+ const DOT_LETTER_SELECTED_OUTLINE_DETAIL_WIDTH_PX = 1.5;
+ const DOT_LETTER_UNSELECTED_OUTLINE_WIDTH_PX = 1;
+ const DOT_LETTER_FONT_WEIGHT = 400;
+ const DOT_LETTER_FONT_SIZE_PX = 9;
+ const DOT_LETTER_FONT = 'Arial';
+
+ /**
+ * Visual components of a ChartSeries.
+ * @enum
+ */
+ const ChartSeriesComponent = {
+ BACKGROUND: 0,
+ LINE: 1,
+ DOTS: 2
+ };
+
+ /**
+ * A series of points corresponding to a single chart on a chart track.
+ * This class is responsible for drawing the actual chart onto canvas.
+ *
+ * @constructor
+ */
+ function ChartSeries(points, seriesYAxis, opt_renderingConfig) {
+ this.points = points;
+ this.seriesYAxis = seriesYAxis;
+
+ this.useRenderingConfig_(opt_renderingConfig);
+ }
+
+ ChartSeries.prototype = {
+ useRenderingConfig_(opt_renderingConfig) {
+ const config = opt_renderingConfig || {};
+
+ // Store all configuration flags as private properties.
+ for (const [key, defaultValue] of
+ Object.entries(DEFAULT_RENDERING_CONFIG)) {
+ let value = config[key];
+ if (value === undefined) {
+ value = defaultValue;
+ }
+ this[key + '_'] = value;
+ }
+
+ // Avoid unnecessary recomputation in getters.
+ this.topPadding = this.bottomPadding = Math.max(
+ this.selectedPointSize_, this.unselectedPointSize_) / 2;
+ },
+
+ get range() {
+ const range = new tr.b.math.Range();
+ this.points.forEach(function(point) {
+ range.addValue(point.y);
+ }, this);
+ return range;
+ },
+
+ draw(ctx, transform, highDetails) {
+ if (this.points === undefined || this.points.length === 0) {
+ return;
+ }
+
+ // Draw the background.
+ if (this.chartType_ === ChartSeriesType.AREA) {
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.BACKGROUND,
+ highDetails);
+ }
+
+ // Draw the line at the top.
+ if (this.chartType_ === ChartSeriesType.LINE || highDetails) {
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.LINE,
+ highDetails);
+ }
+
+ // Draw the points.
+ this.drawComponent_(ctx, transform, ChartSeriesComponent.DOTS,
+ highDetails);
+ },
+
+ drawComponent_(ctx, transform, component, highDetails) {
+ // We need to consider extra pixels outside the visible area to avoid
+ // visual glitches due to non-zero width of dots.
+ let extraPixels = 0;
+ if (component === ChartSeriesComponent.DOTS) {
+ extraPixels = Math.max(
+ this.selectedPointSize_, this.unselectedPointSize_);
+ }
+ const pixelRatio = transform.pixelRatio;
+ const leftViewX = transform.leftViewX - extraPixels * pixelRatio;
+ const rightViewX = transform.rightViewX + extraPixels * pixelRatio;
+ const leftTimestamp = transform.leftTimestamp - extraPixels;
+ const rightTimestamp = transform.rightTimestamp + extraPixels;
+
+ // Find the index of the first and last (partially) visible points.
+ const firstVisibleIndex = tr.b.findLowIndexInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ leftTimestamp);
+ let lastVisibleIndex = tr.b.findLowIndexInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ rightTimestamp);
+ if (lastVisibleIndex >= this.points.length ||
+ this.points[lastVisibleIndex].x > rightTimestamp) {
+ lastVisibleIndex--;
+ }
+
+ // Pre-calculate component style which does not depend on individual
+ // points:
+ // * Skip distance between points,
+ // * Selected (circle) and unselected (square) dot size,
+ // * Unselected dot opacity,
+ // * Selected dot edge color and width, and
+ // * Line component color and width.
+ const viewSkipDistance = this.skipDistance_ * pixelRatio;
+ let selectedCircleRadius;
+ let letterDotRadius;
+ let squareSize;
+ let squareHalfSize;
+ let squareOpacity;
+ let unselectedSeriesColor;
+ let currentStateSeriesColor;
+
+ ctx.save();
+ ctx.font =
+ DOT_LETTER_FONT_WEIGHT + ' ' +
+ Math.floor(DOT_LETTER_FONT_SIZE_PX * pixelRatio) + 'px ' +
+ DOT_LETTER_FONT;
+ ctx.textBaseline = 'middle';
+ ctx.textAlign = 'center';
+
+ switch (component) {
+ case ChartSeriesComponent.DOTS: {
+ // Selected (circle) and unselected (square) dot size.
+ selectedCircleRadius =
+ (this.selectedPointSize_ / 2) * pixelRatio;
+ letterDotRadius =
+ Math.max(selectedCircleRadius, DOT_LETTER_RADIUS_PX * pixelRatio);
+ squareSize = this.unselectedPointSize_ * pixelRatio;
+ squareHalfSize = squareSize / 2;
+ unselectedSeriesColor = EventPresenter.getCounterSeriesColor(
+ this.colorId_, SelectionState.NONE);
+
+ // Unselected dot opacity.
+ if (!highDetails) {
+ // Unselected dots are not displayed in 'low details' mode.
+ squareOpacity = 0;
+ break;
+ }
+ const visibleIndexRange = lastVisibleIndex - firstVisibleIndex;
+ if (visibleIndexRange <= 0) {
+ // There is at most one visible point.
+ squareOpacity = 1;
+ break;
+ }
+ const visibleViewXRange =
+ transform.worldXToViewX(this.points[lastVisibleIndex].x) -
+ transform.worldXToViewX(this.points[firstVisibleIndex].x);
+ if (visibleViewXRange === 0) {
+ // Multiple visible points which all have the same timestamp.
+ squareOpacity = 1;
+ break;
+ }
+ const density = visibleIndexRange / visibleViewXRange;
+ const clampedDensity = tr.b.math.clamp(density,
+ this.unselectedPointDensityOpaque_,
+ this.unselectedPointDensityTransparent_);
+ const densityRange = this.unselectedPointDensityTransparent_ -
+ this.unselectedPointDensityOpaque_;
+ squareOpacity =
+ (this.unselectedPointDensityTransparent_ - clampedDensity) /
+ densityRange;
+ break;
+ }
+
+ case ChartSeriesComponent.LINE:
+ // Line component color and width.
+ ctx.strokeStyle = EventPresenter.getCounterSeriesColor(
+ this.colorId_, SelectionState.NONE);
+ ctx.lineWidth = this.lineWidth_ * pixelRatio;
+ break;
+
+ case ChartSeriesComponent.BACKGROUND:
+ // Style depends on the selection state of individual points.
+ break;
+
+ default:
+ throw new Error('Invalid component: ' + component);
+ }
+
+ // The main loop which draws the given component of visible points from
+ // left to right. Given the potentially large number of points to draw,
+ // it should be considered performance-critical and function calls should
+ // be avoided when possible.
+ //
+ // Note that the background and line components are drawn in a delayed
+ // fashion: the rectangle/line that we draw in an iteration corresponds
+ // to the *previous* point. This does not apply to the dots, whose
+ // position is independent of the surrounding dots.
+ let previousViewX = undefined;
+ let previousViewY = undefined;
+ let previousViewYBase = undefined;
+ let lastSelectionState = undefined;
+ let baseSteps = undefined;
+ const startIndex = Math.max(firstVisibleIndex - 1, 0);
+ let currentViewX;
+
+ for (let i = startIndex; i < this.points.length; i++) {
+ const currentPoint = this.points[i];
+ currentViewX = transform.worldXToViewX(currentPoint.x);
+
+ // Stop drawing the points once we are to the right of the visible area.
+ if (currentViewX > rightViewX) {
+ if (previousViewX !== undefined) {
+ previousViewX = currentViewX = rightViewX;
+ if (component === ChartSeriesComponent.BACKGROUND ||
+ component === ChartSeriesComponent.LINE) {
+ ctx.lineTo(currentViewX, previousViewY);
+ }
+ }
+ break;
+ }
+
+ if (i + 1 < this.points.length) {
+ const nextPoint = this.points[i + 1];
+ const nextViewX = transform.worldXToViewX(nextPoint.x);
+
+ // Skip points that are too close to each other.
+ if (previousViewX !== undefined &&
+ nextViewX - previousViewX <= viewSkipDistance &&
+ nextViewX < rightViewX) {
+ continue;
+ }
+
+ // Start drawing right at the left side of the visible are (instead
+ // of potentially very far to the left).
+ if (currentViewX < leftViewX) {
+ currentViewX = leftViewX;
+ }
+ }
+
+ if (previousViewX !== undefined &&
+ currentViewX - previousViewX < viewSkipDistance) {
+ // We know that nextViewX > previousViewX + viewSkipDistance, so we
+ // can safely move this points's x over that much without passing
+ // nextViewX. This ensures that the previous point is visible when
+ // zoomed out very far.
+ currentViewX = previousViewX + viewSkipDistance;
+ }
+
+ const currentViewY = Math.round(transform.worldYToViewY(
+ currentPoint.y));
+ let currentViewYBase;
+ if (currentPoint.yBase === undefined) {
+ currentViewYBase = transform.outerBottomViewY;
+ } else {
+ currentViewYBase = Math.round(
+ transform.worldYToViewY(currentPoint.yBase));
+ }
+ const currentSelectionState = currentPoint.selectionState;
+ if (currentSelectionState !== lastSelectionState) {
+ const opacity = currentSelectionState === SelectionState.SELECTED ?
+ 1 : squareOpacity;
+ currentStateSeriesColor = EventPresenter.getCounterSeriesColor(
+ this.colorId_, currentSelectionState, opacity);
+ }
+
+ // Actually draw the given component of the point.
+ switch (component) {
+ case ChartSeriesComponent.DOTS:
+ // Draw the dot for the current point.
+ if (currentPoint.dotLetter) {
+ ctx.fillStyle = unselectedSeriesColor;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('black');
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY,
+ letterDotRadius + DOT_LETTER_RADIUS_PADDING_PX, 0,
+ 2 * Math.PI);
+ ctx.fill();
+ if (currentSelectionState === SelectionState.SELECTED) {
+ ctx.lineWidth = DOT_LETTER_SELECTED_OUTLINE_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('olive');
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY, letterDotRadius, 0,
+ 2 * Math.PI);
+ ctx.lineWidth = DOT_LETTER_SELECTED_OUTLINE_DETAIL_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('yellow');
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = DOT_LETTER_UNSELECTED_OUTLINE_WIDTH_PX;
+ ctx.strokeStyle =
+ ColorScheme.getColorForReservedNameAsString('black');
+ ctx.stroke();
+ }
+ ctx.fillStyle =
+ ColorScheme.getColorForReservedNameAsString('white');
+ ctx.fillText(currentPoint.dotLetter, currentViewX, currentViewY);
+ } else {
+ ctx.strokeStyle = unselectedSeriesColor;
+ ctx.lineWidth = pixelRatio;
+ if (currentSelectionState === SelectionState.SELECTED) {
+ if (this.solidSelectedDots_) {
+ ctx.fillStyle = ctx.strokeStyle;
+ } else {
+ ctx.fillStyle = currentStateSeriesColor;
+ }
+
+ ctx.beginPath();
+ ctx.arc(currentViewX, currentViewY, selectedCircleRadius, 0,
+ 2 * Math.PI);
+ ctx.fill();
+ ctx.stroke();
+ } else if (squareOpacity > 0) {
+ ctx.fillStyle = currentStateSeriesColor;
+ ctx.fillRect(currentViewX - squareHalfSize,
+ currentViewY - squareHalfSize, squareSize, squareSize);
+ }
+ }
+ break;
+
+ case ChartSeriesComponent.LINE:
+ // Draw the top line for the previous point (if applicable), or
+ // prepare for drawing the top line of the current point in the next
+ // iteration.
+ if (previousViewX === undefined) {
+ ctx.beginPath();
+ ctx.moveTo(currentViewX, currentViewY);
+ } else if (this.stepGraph_) {
+ ctx.lineTo(currentViewX, previousViewY);
+ }
+
+ // Move to the current point coordinate.
+ ctx.lineTo(currentViewX, currentViewY);
+ break;
+
+ case ChartSeriesComponent.BACKGROUND:
+ // Draw the background for the previous point (if applicable).
+ if (previousViewX !== undefined && this.stepGraph_) {
+ ctx.lineTo(currentViewX, previousViewY);
+ } else {
+ ctx.lineTo(currentViewX, currentViewY);
+ }
+
+ // Finish the bottom part of the backgound polygon, change
+ // background color and start a new polygon when the selection state
+ // changes (and at the beginning).
+ if (currentSelectionState !== lastSelectionState) {
+ if (previousViewX !== undefined) {
+ let previousBaseStepViewX = currentViewX;
+ for (let j = baseSteps.length - 1; j >= 0; j--) {
+ const baseStep = baseSteps[j];
+ const baseStepViewX = baseStep.viewX;
+ const baseStepViewY = baseStep.viewY;
+ ctx.lineTo(previousBaseStepViewX, baseStepViewY);
+ ctx.lineTo(baseStepViewX, baseStepViewY);
+ previousBaseStepViewX = baseStepViewX;
+ }
+ ctx.closePath();
+ ctx.fill();
+ }
+ ctx.beginPath();
+ ctx.fillStyle = EventPresenter.getCounterSeriesColor(
+ this.colorId_, currentSelectionState,
+ this.backgroundOpacity_);
+ ctx.moveTo(currentViewX, currentViewYBase);
+ baseSteps = [];
+ }
+
+ if (currentViewYBase !== previousViewYBase ||
+ currentSelectionState !== lastSelectionState) {
+ baseSteps.push({viewX: currentViewX, viewY: currentViewYBase});
+ }
+
+ // Move to the current point coordinate.
+ ctx.lineTo(currentViewX, currentViewY);
+ break;
+
+ default:
+ throw new Error('Not reachable');
+ }
+
+ previousViewX = currentViewX;
+ previousViewY = currentViewY;
+ previousViewYBase = currentViewYBase;
+ lastSelectionState = currentSelectionState;
+ }
+
+ // If we still have an open background or top line polygon (which is
+ // always the case once we have started drawing due to the delayed fashion
+ // of drawing), we must close it.
+ if (previousViewX !== undefined) {
+ switch (component) {
+ case ChartSeriesComponent.DOTS:
+ // All dots were drawn in the main loop.
+ break;
+
+ case ChartSeriesComponent.LINE:
+ ctx.stroke();
+ break;
+
+ case ChartSeriesComponent.BACKGROUND: {
+ let previousBaseStepViewX = currentViewX;
+ for (let j = baseSteps.length - 1; j >= 0; j--) {
+ const baseStep = baseSteps[j];
+ const baseStepViewX = baseStep.viewX;
+ const baseStepViewY = baseStep.viewY;
+ ctx.lineTo(previousBaseStepViewX, baseStepViewY);
+ ctx.lineTo(baseStepViewX, baseStepViewY);
+ previousBaseStepViewX = baseStepViewX;
+ }
+ ctx.closePath();
+ ctx.fill();
+ break;
+ }
+
+ default:
+ throw new Error('Not reachable');
+ }
+ }
+ ctx.restore();
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ const points = this.points;
+
+ function getPointWidth(point, i) {
+ if (i === points.length - 1) {
+ return LAST_POINT_WIDTH * viewPixWidthWorld;
+ }
+ const nextPoint = points[i + 1];
+ return nextPoint.x - point.x;
+ }
+
+ function selectPoint(point) {
+ point.addToSelection(selection);
+ }
+
+ tr.b.iterateOverIntersectingIntervals(
+ this.points,
+ function(point) { return point.x; },
+ getPointWidth,
+ loWX,
+ hiWX,
+ selectPoint);
+ },
+
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (this.points === undefined) return false;
+
+ const index = this.points.findIndex(point => point.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex < 0 || newIndex >= this.points.length) return false;
+
+ this.points[newIndex].addToSelection(selection);
+ return true;
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ if (this.points === undefined) return;
+
+ const item = tr.b.findClosestElementInSortedArray(
+ this.points,
+ function(point) { return point.x; },
+ worldX,
+ worldMaxDist);
+
+ if (!item) return;
+
+ item.addToSelection(selection);
+ }
+ };
+
+ return {
+ ChartSeries,
+ ChartSeriesType,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html
new file mode 100644
index 00000000000..b07e4276e26
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_test.html
@@ -0,0 +1,331 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const Event = tr.model.Event;
+ const SelectionState = tr.model.SelectionState;
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const ChartTransform = tr.ui.tracks.ChartTransform;
+ const ChartSeriesType = tr.ui.tracks.ChartSeriesType;
+
+ const CANVAS_WIDTH = 800;
+ const CANVAS_HEIGHT = 80;
+
+ function getSelectionStateForTesting(index) {
+ index = index % 7;
+ if (index < 5) {
+ return SelectionState.getFromBrighteningLevel(index % 4);
+ }
+ return SelectionState.getFromDimmingLevel(index % 3);
+ }
+
+ function buildSeries(renderingConfig) {
+ const points = [];
+ for (let i = 0; i < 60; i++) {
+ const event = new Event();
+ event.index = i;
+ const phase = i * Math.PI / 15;
+ const value = Math.sin(phase);
+ const peakIndex = Math.floor((phase + Math.PI / 2) / (2 * Math.PI));
+ const base = peakIndex % 2 === 0 ? undefined : -1 + value / 1.5;
+ const point = new ChartPoint(event, i - 30, value, base);
+ points.push(point);
+ }
+ const seriesYAxis = new ChartSeriesYAxis(-1, 1);
+ return new ChartSeries(points, seriesYAxis, renderingConfig);
+ }
+
+ function drawSeriesWithDetails(test, series, highDetails) {
+ const div = document.createElement('div');
+ const canvas = document.createElement('canvas');
+ Polymer.dom(div).appendChild(canvas);
+
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ canvas.width = CANVAS_WIDTH * pixelRatio;
+ canvas.style.width = CANVAS_WIDTH + 'px';
+ canvas.height = CANVAS_HEIGHT * pixelRatio;
+ canvas.style.height = CANVAS_HEIGHT + 'px';
+
+ const displayTransform = new TimelineDisplayTransform();
+ displayTransform.scaleX = CANVAS_WIDTH * pixelRatio / 60;
+ displayTransform.panX = 30;
+
+ const transform = new ChartTransform(
+ displayTransform,
+ series.seriesYAxis,
+ CANVAS_WIDTH * pixelRatio,
+ CANVAS_HEIGHT * pixelRatio,
+ 10 * pixelRatio,
+ 10 * pixelRatio,
+ pixelRatio);
+
+ series.draw(canvas.getContext('2d'), transform, highDetails);
+
+ test.addHTMLOutput(div);
+ }
+
+ function drawSeries(test, series) {
+ drawSeriesWithDetails(test, series, false);
+ drawSeriesWithDetails(test, series, true);
+ series.stepGraph_ = !series.stepGraph_;
+ drawSeriesWithDetails(test, series, false);
+ drawSeriesWithDetails(test, series, true);
+ }
+
+ test('instantiate_defaultConfig', function() {
+ const series = buildSeries(undefined);
+ drawSeries(this, series);
+ });
+
+ test('instantiate_lineChart', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.LINE,
+ colorId: 4,
+ unselectedPointSize: 6,
+ lineWidth: 2,
+ unselectedPointDensityOpaque: 0.08
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_areaChart', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 2,
+ backgroundOpacity: 0.2
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_largeSkipDistance', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 1,
+ skipDistance: 40,
+ unselectedPointDensityTransparent: 0.07
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selection', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithSolidDots', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ solidSelectedDots: true,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithAllConfigFlags', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ colorId: 15,
+ lineWidth: 2,
+ skipDistance: 25,
+ unselectedPointDensityOpaque: 0.07,
+ unselectedPointDensityTransparent: 0.09,
+ backgroundOpacity: 0.8
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ });
+ drawSeries(this, series);
+ });
+
+ test('instantiate_selectionWithDotLetters', function() {
+ const series = buildSeries({
+ chartType: ChartSeriesType.AREA,
+ selectedPointSize: 10,
+ unselectedPointSize: 6,
+ solidSelectedDots: true,
+ colorId: 10
+ });
+ series.points.forEach(function(point, index) {
+ point.modelItem.selectionState = getSelectionStateForTesting(index);
+ if (index % 10 === 3) {
+ point.dotLetter = 'P';
+ } else if (index % 10 === 7) {
+ point.dotLetter = '\u26A0';
+ }
+ });
+ drawSeries(this, series);
+ });
+
+ test('checkRange', function() {
+ const series = buildSeries();
+ const range = series.range;
+ assert.isFalse(range.isEmpty);
+ assert.closeTo(range.min, -1, 0.05);
+ assert.closeTo(range.max, 1, 0.05);
+ });
+
+ test('checkaddIntersectingEventsInRangeToSelectionInWorldSpace', function() {
+ const series = buildSeries();
+
+ // Too far left.
+ let sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -1000, -30.5, 40, sel);
+ assert.lengthOf(sel, 0);
+
+ // Select first point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -30.5, -29.5, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 0);
+
+ // Select second point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -28.8, -28.2, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 1);
+
+ // Select points in the middle.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -0.99, 1.01, 40, sel);
+ assert.lengthOf(sel, 3);
+ const iterator = sel[Symbol.iterator]();
+ assert.strictEqual(iterator.next().value.index, 29);
+ assert.strictEqual(iterator.next().value.index, 30);
+ assert.strictEqual(iterator.next().value.index, 31);
+
+ // Select the last point.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 668.99, 668.99, 40, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 59);
+
+ // Too far right.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 669.01, 2000, 40, sel);
+ assert.lengthOf(sel, 0);
+
+ // Select everything.
+ sel = new EventSet();
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -29.01, 669.01, 40, sel);
+ assert.lengthOf(sel, 60);
+ });
+
+ test('checkaddEventNearToProvidedEventToSelection', function() {
+ const series = buildSeries();
+
+ // Invalid event.
+ let sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ new Event(), 1, sel));
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ new Event(), -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // First point.
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[0].modelItem, 1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 1);
+
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ series.points[0].modelItem, -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // Middle point.
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[30].modelItem, 1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 31);
+
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[30].modelItem, -1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 29);
+
+ // Last point.
+ sel = new EventSet();
+ assert.isFalse(series.addEventNearToProvidedEventToSelection(
+ series.points[59].modelItem, 1, sel));
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ assert.isTrue(series.addEventNearToProvidedEventToSelection(
+ series.points[59].modelItem, -1, sel));
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 58);
+ });
+
+ test('checkAddClosestEventToSelection', function() {
+ const series = buildSeries();
+
+ // Left of first point.
+ let sel = new EventSet();
+ series.addClosestEventToSelection(-40, 9, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(-40, 11, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 0);
+
+ // Between two points.
+ sel = new EventSet();
+ series.addClosestEventToSelection(0.4, 0.3, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(0.4, 0.4, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 30);
+
+ // Right of last point.
+ sel = new EventSet();
+ series.addClosestEventToSelection(40, 10, -0.5, 0.5, sel);
+ assert.lengthOf(sel, 0);
+
+ sel = new EventSet();
+ series.addClosestEventToSelection(40, 12, -0.5, 0.5, sel);
+ assert.strictEqual(tr.b.getOnlyElement(sel).index, 59);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html
new file mode 100644
index 00000000000..f34b4c68579
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/unit.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const IDEAL_MAJOR_MARK_HEIGHT_PX = 30;
+ const AXIS_LABLE_MARGIN_PX = 10;
+ const AXIS_LABLE_FONT_SIZE_PX = 9;
+ const AXIS_LABLE_FONT = 'Arial';
+
+ /**
+ * A vertical axis for a (set of) chart series which maps an arbitrary range
+ * of values [min, max] to the unit range [0, 1].
+ *
+ * @constructor
+ */
+ function ChartSeriesYAxis(opt_min, opt_max) {
+ this.guid_ = tr.b.GUID.allocateSimple();
+ this.bounds = new tr.b.math.Range();
+ if (opt_min !== undefined) this.bounds.addValue(opt_min);
+ if (opt_max !== undefined) this.bounds.addValue(opt_max);
+ }
+
+ ChartSeriesYAxis.prototype = {
+ get guid() {
+ return this.guid_;
+ },
+
+ valueToUnitRange(value) {
+ if (this.bounds.isEmpty) {
+ throw new Error('Chart series y-axis bounds are empty');
+ }
+ const bounds = this.bounds;
+ if (bounds.range === 0) return 0;
+ return (value - bounds.min) / bounds.range;
+ },
+
+ unitRangeToValue(unitRange) {
+ if (this.bounds.isEmpty) {
+ throw new Error('Chart series y-axis bounds are empty');
+ }
+ return unitRange * this.bounds.range + this.bounds.min;
+ },
+
+ /**
+ * Automatically set the y-axis bounds from the range of values of all
+ * series in a list.
+ *
+ * See the description of autoSetFromRange for the optional configuration
+ * argument flags.
+ */
+ autoSetFromSeries(series, opt_config) {
+ const range = new tr.b.math.Range();
+ series.forEach(function(s) {
+ range.addRange(s.range);
+ }, this);
+ this.autoSetFromRange(range, opt_config);
+ },
+
+ /**
+ * Automatically set the y-axis bound from a range of values.
+ *
+ * The following four flags, which affect the behavior of this method with
+ * respect to already defined bounds, can be present in the optional
+ * configuration (a flag is assumed to be false if it is not provided or if
+ * the configuration is not provided):
+ *
+ * - expandMin: allow decreasing the min bound (if range.min < this.min)
+ * - shrinkMin: allow increasing the min bound (if range.min > this.min)
+ * - expandMax: allow increasing the max bound (if range.max > this.max)
+ * - shrinkMax: allow decreasing the max bound (if range.max < this.max)
+ *
+ * This method will ensure that the resulting bounds are defined and valid
+ * (i.e. min <= max) provided that they were valid or empty before and the
+ * value range is non-empty and valid.
+ *
+ * Note that unless expanding/shrinking a bound is explicitly enabled in
+ * the configuration, non-empty bounds will not be changed under any
+ * circumstances.
+ *
+ * Observe that if no configuration is provided (or all flags are set to
+ * false), this method will only modify the y-axis bounds if they are empty.
+ */
+ autoSetFromRange(range, opt_config) {
+ if (range.isEmpty) return;
+
+ const bounds = this.bounds;
+ if (bounds.isEmpty) {
+ bounds.addRange(range);
+ return;
+ }
+
+ if (!opt_config) return;
+
+ const useRangeMin = (opt_config.expandMin && range.min < bounds.min ||
+ opt_config.shrinkMin && range.min > bounds.min);
+ const useRangeMax = (opt_config.expandMax && range.max > bounds.max ||
+ opt_config.shrinkMax && range.max < bounds.max);
+
+ // Neither bound is modified.
+ if (!useRangeMin && !useRangeMax) return;
+
+ // Both bounds are modified. Assuming the range argument is a valid
+ // range, no extra checks are necessary.
+ if (useRangeMin && useRangeMax) {
+ bounds.min = range.min;
+ bounds.max = range.max;
+ return;
+ }
+
+ // Only one bound is modified. We must ensure that it doesn't go
+ // over/under the other (unmodified) bound.
+ if (useRangeMin) {
+ bounds.min = Math.min(range.min, bounds.max);
+ } else {
+ bounds.max = Math.max(range.max, bounds.min);
+ }
+ },
+
+
+ majorMarkHeightWorld_(transform, pixelRatio) {
+ const idealMajorMarkHeightPx = IDEAL_MAJOR_MARK_HEIGHT_PX * pixelRatio;
+ const idealMajorMarkHeightWorld =
+ transform.vectorToWorldDistance(idealMajorMarkHeightPx);
+
+ return tr.b.math.preferredNumberLargerThanMin(idealMajorMarkHeightWorld);
+ },
+
+ draw(ctx, transform, showYAxisLabels, showYGridLines) {
+ if (!showYAxisLabels && !showYGridLines) return;
+
+ const pixelRatio = transform.pixelRatio;
+ const viewTop = transform.outerTopViewY;
+ const worldTop = transform.viewYToWorldY(viewTop);
+ const viewBottom = transform.outerBottomViewY;
+ const viewHeight = viewBottom - viewTop;
+ const viewLeft = transform.leftViewX;
+ const viewRight = transform.rightViewX;
+ const labelLeft = transform.leftYLabel;
+
+ ctx.save();
+ ctx.lineWidth = pixelRatio;
+ ctx.fillStyle = ColorScheme.getColorForReservedNameAsString('black');
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'center';
+
+ ctx.font =
+ (AXIS_LABLE_FONT_SIZE_PX * pixelRatio) + 'px ' + AXIS_LABLE_FONT;
+
+ // Draw left edge of chart series.
+ ctx.beginPath();
+ ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('black');
+ tr.ui.b.drawLine(
+ ctx, viewLeft, viewTop, viewLeft, viewBottom, viewLeft);
+ ctx.stroke();
+ ctx.closePath();
+
+ // Draw y-axis ticks and gridlines.
+ ctx.beginPath();
+ ctx.strokeStyle = ColorScheme.getColorForReservedNameAsString('grey');
+
+ const majorMarkHeight = this.majorMarkHeightWorld_(transform, pixelRatio);
+ const maxMajorMark = Math.max(transform.viewYToWorldY(viewTop),
+ Math.abs(transform.viewYToWorldY(viewBottom)));
+ for (let curWorldY = 0;
+ curWorldY <= maxMajorMark;
+ curWorldY += majorMarkHeight) {
+ const roundedUnitValue = Math.floor(curWorldY * 1000000) / 1000000;
+ const curViewYPositive = transform.worldYToViewY(curWorldY);
+ if (curViewYPositive >= viewTop) {
+ if (showYAxisLabels) {
+ ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX,
+ curViewYPositive - AXIS_LABLE_MARGIN_PX);
+ }
+ if (showYGridLines) {
+ tr.ui.b.drawLine(
+ ctx, viewLeft, curViewYPositive, viewRight, curViewYPositive);
+ }
+ }
+
+ const curViewYNegative = transform.worldYToViewY(-1 * curWorldY);
+ if (curViewYNegative <= viewBottom) {
+ if (showYAxisLabels) {
+ ctx.fillText(roundedUnitValue, viewLeft + AXIS_LABLE_MARGIN_PX,
+ curViewYNegative - AXIS_LABLE_MARGIN_PX);
+ }
+ if (showYGridLines) {
+ tr.ui.b.drawLine(
+ ctx, viewLeft, curViewYNegative, viewRight, curViewYNegative);
+ }
+ }
+ }
+ ctx.stroke();
+ ctx.restore();
+ }
+ };
+
+ return {
+ ChartSeriesYAxis,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html
new file mode 100644
index 00000000000..4a759e040d4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_series_y_axis_test.html
@@ -0,0 +1,313 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const Range = tr.b.math.Range;
+
+ function buildRange() {
+ const range = new Range();
+ for (let i = 0; i < arguments.length; i++) {
+ range.addValue(arguments[i]);
+ }
+ return range;
+ }
+
+ function buildSeries() {
+ const points = [];
+ for (let i = 0; i < arguments.length; i++) {
+ points.push(new ChartPoint(undefined, i, arguments[i]));
+ }
+ return new ChartSeries(points, new ChartSeriesYAxis());
+ }
+
+ test('instantiate_emptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis();
+ assert.isTrue(seriesYAxis.bounds.isEmpty);
+ });
+
+ test('instantiate_nonEmptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-2, 12);
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -2);
+ assert.strictEqual(seriesYAxis.bounds.max, 12);
+ });
+
+ test('instantiate_equalBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(2.72);
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 2.72);
+ assert.strictEqual(seriesYAxis.bounds.max, 2.72);
+ });
+
+ test('checkValueToUnitRange_emptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis();
+ assert.throws(function() { seriesYAxis.valueToUnitRange(42); });
+ });
+
+ test('checkValueToUnitRange_nonEmptyBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(10, 20);
+
+ assert.strictEqual(seriesYAxis.valueToUnitRange(0), -1);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(10), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(15), 0.5);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(20), 1);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(30), 2);
+ });
+
+ test('checkValueToUnitRange_equalBounds', function() {
+ const seriesYAxis = new ChartSeriesYAxis(3.14);
+
+ assert.strictEqual(seriesYAxis.valueToUnitRange(0), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(3.14), 0);
+ assert.strictEqual(seriesYAxis.valueToUnitRange(6.28), 0);
+ });
+
+ test('checkAutoSetFromRange_emptyBounds', function() {
+ // Empty range.
+ let seriesYAxis = new ChartSeriesYAxis();
+ seriesYAxis.autoSetFromRange(buildRange());
+ assert.isTrue(seriesYAxis.bounds.isEmpty);
+
+ // Non-empty range.
+ seriesYAxis = new ChartSeriesYAxis();
+ seriesYAxis.autoSetFromRange(buildRange(-1, 3));
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -1);
+ assert.strictEqual(seriesYAxis.bounds.max, 3);
+ });
+
+ test('checkAutoSetFromRange_nonEmptyBounds', function() {
+ // Empty range.
+ let seriesYAxis = new ChartSeriesYAxis(0, 1);
+ seriesYAxis.autoSetFromRange(buildRange());
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 1);
+
+ // No configuration.
+ seriesYAxis = new ChartSeriesYAxis(2, 3);
+ seriesYAxis.autoSetFromRange(buildRange(1, 4));
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 2);
+ assert.strictEqual(seriesYAxis.bounds.max, 3);
+
+ // Allow expanding min.
+ seriesYAxis = new ChartSeriesYAxis(-2, -1);
+ seriesYAxis.autoSetFromRange(buildRange(-3, 0), {expandMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -3);
+ assert.strictEqual(seriesYAxis.bounds.max, -1);
+
+ // Allow shrinking min.
+ seriesYAxis = new ChartSeriesYAxis(-2, -1);
+ seriesYAxis.autoSetFromRange(buildRange(-1.5, 0.5), {shrinkMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, -1.5);
+ assert.strictEqual(seriesYAxis.bounds.max, -1);
+
+ seriesYAxis = new ChartSeriesYAxis(7, 8);
+ seriesYAxis.autoSetFromRange(buildRange(9, 10), {shrinkMin: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 8);
+ assert.strictEqual(seriesYAxis.bounds.max, 8);
+
+ // Allow expanding max.
+ seriesYAxis = new ChartSeriesYAxis(19, 20);
+ seriesYAxis.autoSetFromRange(buildRange(18, 21), {expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 19);
+ assert.strictEqual(seriesYAxis.bounds.max, 21);
+
+ // Allow shrinking max.
+ seriesYAxis = new ChartSeriesYAxis(30, 32);
+ seriesYAxis.autoSetFromRange(buildRange(29, 31), {shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 30);
+ assert.strictEqual(seriesYAxis.bounds.max, 31);
+
+ seriesYAxis = new ChartSeriesYAxis(41, 42);
+ seriesYAxis.autoSetFromRange(buildRange(39, 40), {shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 41);
+ assert.strictEqual(seriesYAxis.bounds.max, 41);
+
+ // Allow shrinking both bounds.
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(51, 52),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 51);
+ assert.strictEqual(seriesYAxis.bounds.max, 52);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(49, 52),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 50);
+ assert.strictEqual(seriesYAxis.bounds.max, 52);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(51, 54),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 51);
+ assert.strictEqual(seriesYAxis.bounds.max, 53);
+
+ seriesYAxis = new ChartSeriesYAxis(50, 53);
+ seriesYAxis.autoSetFromRange(buildRange(49, 54),
+ {shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 50);
+ assert.strictEqual(seriesYAxis.bounds.max, 53);
+
+ // Allow expanding both bounds.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(0, 100),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.5, 100),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(0, 60.5),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {expandMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ // Allow shrinking min and expanding max.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(62, 63),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 62);
+ assert.strictEqual(seriesYAxis.bounds.max, 63);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 63),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 63);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60.2);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 60.5),
+ {shrinkMin: true, expandMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ // Allow expanding min and shrinking max.
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(62, 63),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 63),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 59);
+ assert.strictEqual(seriesYAxis.bounds.max, 61);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(60.2, 60.8),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 60);
+ assert.strictEqual(seriesYAxis.bounds.max, 60.8);
+
+ seriesYAxis = new ChartSeriesYAxis(60, 61);
+ seriesYAxis.autoSetFromRange(buildRange(59, 60.5),
+ {expandMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 59);
+ assert.strictEqual(seriesYAxis.bounds.max, 60.5);
+
+ // Allow everything.
+ seriesYAxis = new ChartSeriesYAxis(200, 250);
+ seriesYAxis.autoSetFromRange(buildRange(150, 175),
+ {expandMin: true, expandMax: true, shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 150);
+ assert.strictEqual(seriesYAxis.bounds.max, 175);
+
+ seriesYAxis = new ChartSeriesYAxis(0, 0.1);
+ seriesYAxis.autoSetFromRange(buildRange(0.2, 0.3),
+ {expandMin: true, expandMax: true, shrinkMin: true, shrinkMax: true});
+ assert.isFalse(seriesYAxis.bounds.isEmpty);
+ assert.strictEqual(seriesYAxis.bounds.min, 0.2);
+ assert.strictEqual(seriesYAxis.bounds.max, 0.3);
+ });
+
+ test('checkAutoSetFromSeries_noSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [];
+
+ seriesYAxis.autoSetFromSeries(series);
+ assert.strictEqual(seriesYAxis.bounds.min, -100);
+ assert.strictEqual(seriesYAxis.bounds.max, 100);
+ });
+
+ test('checkAutoSetFromSeries_oneSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [buildSeries(-80, 100, -40, 200)];
+
+ seriesYAxis.autoSetFromSeries(series, {shrinkMin: true, expandMax: true});
+ assert.strictEqual(seriesYAxis.bounds.min, -80);
+ assert.strictEqual(seriesYAxis.bounds.max, 200);
+ });
+
+ test('checkAutoSetFromSeries_multipleSeries', function() {
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+ const series = [
+ buildSeries(0, 20, 10, 30),
+ buildSeries(),
+ buildSeries(-500)
+ ];
+
+ seriesYAxis.autoSetFromSeries(series, {expandMin: true, shrinkMax: true});
+ assert.strictEqual(seriesYAxis.bounds.min, -500);
+ assert.strictEqual(seriesYAxis.bounds.max, 30);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html
new file mode 100644
index 00000000000..58ef08d651c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track.html
@@ -0,0 +1,281 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.chart-track {
+ height: 30px;
+ position: relative;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a chart.
+ *
+ * @constructor
+ * @extends {Track}
+ */
+ const ChartTrack =
+ tr.ui.b.define('chart-track', tr.ui.tracks.Track);
+
+ ChartTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('chart-track');
+ this.series_ = undefined;
+ this.axes_ = undefined;
+
+ // GUID -> {axis: ChartSeriesYAxis, series: [ChartSeries]}.
+ this.axisGuidToAxisData_ = undefined;
+
+ // The maximum top and bottom padding of all series.
+ this.topPadding_ = undefined;
+ this.bottomPadding_ = undefined;
+
+ this.showYAxisLabels_ = undefined;
+ this.showGridLines_ = undefined;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get series() {
+ return this.series_;
+ },
+
+ /**
+ * Set the list of chart series to be displayed on this track. The list
+ * is assumed to be sorted in increasing z-order (i.e. the last series in
+ * the list will be drawn at the top).
+ */
+ set series(series) {
+ this.series_ = series;
+ this.calculateAxisDataAndPadding_();
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ this.invalidateDrawingContainer();
+ },
+
+ get showYAxisLabels() {
+ return this.showYAxisLabels_;
+ },
+
+ set showYAxisLabels(showYAxisLabels) {
+ this.showYAxisLabels_ = showYAxisLabels;
+ this.invalidateDrawingContainer();
+ },
+
+ get showGridLines() {
+ return this.showGridLines_;
+ },
+
+ set showGridLines(showGridLines) {
+ this.showGridLines_ = showGridLines;
+ this.invalidateDrawingContainer();
+ },
+
+ get hasVisibleContent() {
+ return !!this.series && this.series.length > 0;
+ },
+
+ calculateAxisDataAndPadding_() {
+ if (!this.series_) {
+ this.axes_ = undefined;
+ this.axisGuidToAxisData_ = undefined;
+ this.topPadding_ = undefined;
+ this.bottomPadding_ = undefined;
+ return;
+ }
+
+ const axisGuidToAxisData = {};
+ let topPadding = 0;
+ let bottomPadding = 0;
+
+ this.series_.forEach(function(series) {
+ const seriesYAxis = series.seriesYAxis;
+ const axisGuid = seriesYAxis.guid;
+ if (!(axisGuid in axisGuidToAxisData)) {
+ axisGuidToAxisData[axisGuid] = {
+ axis: seriesYAxis,
+ series: []
+ };
+ if (!this.axes_) this.axes_ = [];
+ this.axes_.push(seriesYAxis);
+ }
+ axisGuidToAxisData[axisGuid].series.push(series);
+ topPadding = Math.max(topPadding, series.topPadding);
+ bottomPadding = Math.max(bottomPadding, series.bottomPadding);
+ }, this);
+
+ this.axisGuidToAxisData_ = axisGuidToAxisData;
+ this.topPadding_ = topPadding;
+ this.bottomPadding_ = bottomPadding;
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawChart_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawChart_(viewLWorld, viewRWorld) {
+ if (!this.series_) return;
+
+ const ctx = this.context();
+
+ // Get track drawing parameters.
+ const displayTransform = this.viewport.currentDisplayTransform;
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const highDetails = this.viewport.highDetails;
+
+ // Pre-multiply all device-independent pixel parameters with the pixel
+ // ratio to avoid unnecessary recomputation in the performance-critical
+ // drawing code.
+ const width = bounds.width * pixelRatio;
+ const height = bounds.height * pixelRatio;
+ const topPadding = this.topPadding_ * pixelRatio;
+ const bottomPadding = this.bottomPadding_ * pixelRatio;
+
+ // Set up clipping.
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(0, 0, width, height);
+ ctx.clip();
+
+ // TODO(aiolos): Add support for secondary y-axis on right side of chart.
+ // https://github.com/catapult-project/catapult/issues/3008
+ // Draw y-axis grid lines.
+ if (this.axes_) {
+ if ((this.showGridLines_ || this.showYAxisLabels_) &&
+ this.axes_.length > 1) {
+ throw new Error('Only one axis allowed when showing grid lines.');
+ }
+ for (const yAxis of this.axes_) {
+ const chartTransform = new tr.ui.tracks.ChartTransform(
+ displayTransform, yAxis, width, height,
+ topPadding, bottomPadding, pixelRatio);
+ yAxis.draw(
+ ctx, chartTransform, this.showYAxisLabels_, this.showGridLines_);
+ }
+ }
+
+ // Draw all series in the increasing z-order.
+ for (const series of this.series) {
+ const chartTransform = new tr.ui.tracks.ChartTransform(
+ displayTransform, series.seriesYAxis, width, height, topPadding,
+ bottomPadding, pixelRatio);
+ series.draw(ctx, chartTransform, highDetails);
+ }
+
+ // Stop clipping.
+ ctx.restore();
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ // TODO(petrcermak): Consider adding the series to the track map instead
+ // of the track (a potential performance optimization).
+ this.series_.forEach(function(series) {
+ series.points.forEach(function(point) {
+ point.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ this.series_.forEach(function(series) {
+ series.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection);
+ }, this);
+ },
+
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ let foundItem = false;
+ this.series_.forEach(function(series) {
+ foundItem = foundItem || series.addEventNearToProvidedEventToSelection(
+ event, offset, selection);
+ }, this);
+ return foundItem;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ // Do nothing.
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.series_.forEach(function(series) {
+ series.addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }, this);
+ },
+
+ /**
+ * Automatically set the bounds of all axes on this track from the range of
+ * values of all series (in this track) associated with each of them.
+ *
+ * See the description of ChartSeriesYAxis.autoSetFromRange for the optional
+ * configuration argument flags.
+ */
+ autoSetAllAxes(opt_config) {
+ for (const axisData of Object.values(this.axisGuidToAxisData_)) {
+ const seriesYAxis = axisData.axis;
+ const series = axisData.series;
+ seriesYAxis.autoSetFromSeries(series, opt_config);
+ }
+ },
+
+ /**
+ * Automatically set the bounds of the provided axis from the range of
+ * values of all series (in this track) associated with it.
+ *
+ * See the description of ChartSeriesYAxis.autoSetFromRange for the optional
+ * configuration argument flags.
+ */
+ autoSetAxis(seriesYAxis, opt_config) {
+ const series = this.axisGuidToAxisData_[seriesYAxis.guid].series;
+ seriesYAxis.autoSetFromSeries(series, opt_config);
+ }
+ };
+
+ return {
+ ChartTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html
new file mode 100644
index 00000000000..405640a9b2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_track_test.html
@@ -0,0 +1,454 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+ const ChartPoint = tr.ui.tracks.ChartPoint;
+ const ChartSeries = tr.ui.tracks.ChartSeries;
+ const ChartSeriesType = tr.ui.tracks.ChartSeriesType;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+ const Event = tr.model.Event;
+ const EventSet = tr.model.EventSet;
+ const EventToTrackMap = tr.ui.tracks.EventToTrackMap;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ function buildPoint(x, y) {
+ const event = new Event();
+ return new ChartPoint(event, x, y);
+ }
+
+ function buildTrack(opt_args) {
+ const viewport = (opt_args && opt_args.viewport) ?
+ opt_args.viewport : new Viewport(document.createElement('div'));
+
+ const seriesYAxis1 = new ChartSeriesYAxis(0, 2.5);
+
+ const points1 = [
+ buildPoint(-2.5, 2),
+ buildPoint(-1.5, 1),
+ buildPoint(-0.5, 0),
+ buildPoint(0.5, 1),
+ buildPoint(1.5, 2),
+ buildPoint(2.5, 0)
+ ];
+ const renderingConfig1 = {
+ chartType: ChartSeriesType.AREA,
+ colorId: 6,
+ selectedPointSize: 7
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig1.stepGraph = opt_args.stepGraph;
+ }
+ const series1 = new ChartSeries(points1, seriesYAxis1, renderingConfig1);
+
+ const points2 = [
+ buildPoint(-2.3, 0.2),
+ buildPoint(-1.3, 1.2),
+ buildPoint(-0.3, 2.2),
+ buildPoint(0.3, 1.2),
+ buildPoint(1.3, 0.2),
+ buildPoint(2.3, 0)
+ ];
+ const renderingConfig2 = {
+ chartType: ChartSeriesType.AREA,
+ colorId: 4,
+ selectedPointSize: 10
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig2.stepGraph = opt_args.stepGraph;
+ }
+ const series2 = new ChartSeries(points2, seriesYAxis1, renderingConfig2);
+
+ const seriesList = [series1, series2];
+
+ if (!opt_args || !opt_args.singleAxis) {
+ const seriesYAxis2 = new ChartSeriesYAxis(-100, 100);
+ const points3 = [
+ buildPoint(-3, -50),
+ buildPoint(-2.4, -40),
+ buildPoint(-1.8, -30),
+ buildPoint(-1.2, -20),
+ buildPoint(-0.6, -10),
+ buildPoint(0, 0),
+ buildPoint(0.6, 10),
+ buildPoint(1.2, 20),
+ buildPoint(1.8, 30),
+ buildPoint(2.4, 40),
+ buildPoint(3, 50)
+ ];
+ const renderingConfig3 = {
+ chartType: ChartSeriesType.LINE,
+ lineWidth: 2
+ };
+ if (opt_args && opt_args.stepGraph !== undefined) {
+ renderingConfig3.stepGraph = opt_args.stepGraph;
+ }
+ const series3 = new ChartSeries(points3, seriesYAxis2, renderingConfig3);
+ seriesList.push(series3);
+ }
+
+ const track = new ChartTrack(viewport);
+ track.series = seriesList;
+
+ return track;
+ }
+
+ function buildDashboardTrack(opt_viewport) {
+ const viewport = opt_viewport || new Viewport(
+ document.createElement('div'));
+
+ const seriesYAxis = new ChartSeriesYAxis(0, 1.1);
+ const fileUrl = '/test_data/dashboard_test_points.json';
+ const pointsArray = JSON.parse(tr.b.getSync(fileUrl));
+ const points = [];
+ for (let i = 0; i < pointsArray.length; i++) {
+ points.push(buildPoint(pointsArray[i][0], pointsArray[i][1]));
+ }
+ const renderingConfig = {
+ chartType: ChartSeriesType.LINE,
+ lineWidth: 1,
+ stepGraph: false,
+ selectedPointSize: 10,
+ solidSelectedDots: true,
+ highDetail: false,
+ skipDistance: 0.4
+ };
+ const series = new ChartSeries(points, seriesYAxis, renderingConfig);
+
+ const track = new ChartTrack(viewport);
+ track.series = [series];
+
+ return track;
+ }
+
+ test('instantiate_lowDetailsWithoutSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[2].points[3].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_lowDetailsNoStepGraphWithoutSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport, stepGraph: false});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({viewport, stepGraph: false});
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[2].points[3].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionAndYAxisLabels',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showYAxisLabels = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionAndGridLines',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_highDetailsNoStepGraphWithSelectionYAxisLabelsAndGridLines',
+ function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildTrack({
+ viewport,
+ stepGraph: false,
+ singleAxis: true,
+ });
+ track.showYAxisLabels = true;
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+ track.series[1].points[1].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(-3, 3, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '200px';
+ });
+
+ test('instantiate_dashboardChartStyleWithSelection', function() {
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ viewport.highDetails = true;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = buildDashboardTrack(viewport);
+ track.showYAxisLabels = true;
+ track.showGridLines = true;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.series[0].points[40].modelItem.selectionState =
+ SelectionState.SELECTED;
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ dt.xSetWorldBounds(
+ 26610390797802200, 28950000891700000, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ track.height = '100px';
+ });
+
+ test('checkPadding', function() {
+ const track = buildTrack();
+
+ // Padding should be equal to half maximum point size.
+ assert.strictEqual(track.topPadding_, 5);
+ assert.strictEqual(track.bottomPadding_, 5);
+ });
+
+ test('checkAddEventsToTrackMap', function() {
+ const track = buildTrack();
+ const eventToTrackMap = new EventToTrackMap();
+ track.addEventsToTrackMap(eventToTrackMap);
+ assert.lengthOf(Object.keys(eventToTrackMap), 23);
+ });
+
+ test('checkaddIntersectingEventsInRangeToSelectionInWorldSpace', function() {
+ const track = buildTrack();
+
+ const sel = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ -1.1, -0.7, 0.01, sel);
+ assert.lengthOf(sel, 3);
+ const iter = sel[Symbol.iterator]();
+ assert.strictEqual(iter.next().value, track.series[0].points[1].modelItem);
+ assert.strictEqual(iter.next().value, track.series[1].points[1].modelItem);
+ assert.strictEqual(iter.next().value, track.series[2].points[3].modelItem);
+ });
+
+ test('checkaddEventNearToProvidedEventToSelection', function() {
+ const track = buildTrack();
+
+ // Fail to find a near item to the left in any series.
+ let sel = new EventSet();
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ track.series[0].points[0].modelItem, -1, sel));
+ assert.lengthOf(sel, 0);
+
+ // Succeed at finding a near item to the right of one series.
+ sel = new EventSet();
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ track.series[1].points[1].modelItem, 1, sel));
+ assert.strictEqual(
+ tr.b.getOnlyElement(sel), track.series[1].points[2].modelItem);
+ });
+
+ test('checkAddClosestEventToSelection', function() {
+ const track = buildTrack();
+
+ const sel = new EventSet();
+ track.addClosestEventToSelection(-0.8, 0.4, 0.5, 1.5, sel);
+ assert.lengthOf(sel, 2);
+ const iter = sel[Symbol.iterator]();
+ assert.strictEqual(iter.next().value, track.series[0].points[2].modelItem);
+ assert.strictEqual(iter.next().value, track.series[2].points[4].modelItem);
+ });
+
+ test('checkAutoSetAllAxes', function() {
+ const track = buildTrack();
+ const seriesYAxis1 = track.series[0].seriesYAxis;
+ const seriesYAxis2 = track.series[2].seriesYAxis;
+
+ track.autoSetAllAxes({expandMax: true, shrinkMax: true});
+
+ // Min bounds of both axes should not have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.min, 0);
+ assert.strictEqual(seriesYAxis2.bounds.min, -100);
+
+ // Max bounds of both axes should have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.max, 2.2);
+ assert.strictEqual(seriesYAxis2.bounds.max, 50);
+ });
+
+ test('checkAutoSetAxis', function() {
+ const track = buildTrack();
+ const seriesYAxis1 = track.series[0].seriesYAxis;
+ const seriesYAxis2 = track.series[2].seriesYAxis;
+
+ track.autoSetAxis(seriesYAxis2,
+ {expandMin: true, shrinkMin: true, expandMax: true, shrinkMax: true});
+
+ // First axis should not have been modified.
+ assert.strictEqual(seriesYAxis1.bounds.min, 0);
+ assert.strictEqual(seriesYAxis1.bounds.max, 2.5);
+
+ // Second axis should have been modified.
+ assert.strictEqual(seriesYAxis2.bounds.min, -50);
+ assert.strictEqual(seriesYAxis2.bounds.max, 50);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html
new file mode 100644
index 00000000000..f6bf6310116
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A helper object encapsulating all parameters necessary to draw a chart
+ * series and provides conversion between world coordinates and physical
+ * pixels.
+ *
+ * All parameters (except for pixelRatio) are assumed to be in physical pixels
+ * (i.e. already pre-multiplied with pixelRatio).
+ *
+ * The diagram below explains the meaning of the resulting fields with
+ * respect to a chart track:
+ *
+ * outerTopViewY -> +--------------------/-\------+ <- Top padding
+ * innerTopViewY -> + - - - - - - - - - -| |- - - + <- Axis max
+ * | .. ==\-/== |
+ * | === Series === |
+ * | ==/-\== .. |
+ * innerBottomViewY -> + - - -Point- - - - - - - - - + <- Axis min
+ * outerBottomViewY -> +-------\-/-------------------+ <- Bottom padding
+ * ^ ^
+ * leftViewX rightViewX
+ * leftTimeStamp rightTimestamp
+ *
+ * Labels starting with a lower case letter are the resulting fields of the
+ * transform object. Labels starting with an upper case letter correspond
+ * to the relevant chart track concepts.
+ *
+ * @constructor
+ */
+ function ChartTransform(displayTransform, axis, trackWidth,
+ trackHeight, topPadding, bottomPadding, pixelRatio) {
+ this.pixelRatio = pixelRatio;
+
+ // X axis.
+ this.leftViewX = 0;
+ this.rightViewX = trackWidth;
+ this.leftTimestamp = displayTransform.xViewToWorld(this.leftViewX);
+ this.rightTimestamp = displayTransform.xViewToWorld(this.rightViewX);
+
+ this.displayTransform_ = displayTransform;
+
+ // Y axis.
+ this.outerTopViewY = 0;
+ this.innerTopViewY = topPadding;
+ this.innerBottomViewY = trackHeight - bottomPadding;
+ this.outerBottomViewY = trackHeight;
+
+ this.axis_ = axis;
+ this.innerHeight_ = this.innerBottomViewY - this.innerTopViewY;
+ }
+
+ ChartTransform.prototype = {
+ worldXToViewX(worldX) {
+ return this.displayTransform_.xWorldToView(worldX);
+ },
+
+ viewXToWorldX(viewX) {
+ return this.displayTransform_.xViewToWorld(viewX);
+ },
+
+ vectorToWorldDistance(viewY) {
+ return this.axis_.bounds.range * Math.abs(viewY / this.innerHeight_);
+ },
+
+ viewYToWorldY(viewY) {
+ return this.axis_.unitRangeToValue(
+ 1 - (viewY - this.innerTopViewY) / this.innerHeight_);
+ },
+
+ worldYToViewY(worldY) {
+ const innerHeightCoefficient = 1 - this.axis_.valueToUnitRange(worldY);
+ return innerHeightCoefficient * this.innerHeight_ + this.innerTopViewY;
+ }
+ };
+
+ return {
+ ChartTransform,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html
new file mode 100644
index 00000000000..8d46e08aace
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/chart_transform_test.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_display_transform.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_transform.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
+ const ChartTransform = tr.ui.tracks.ChartTransform;
+ const ChartSeriesYAxis = tr.ui.tracks.ChartSeriesYAxis;
+
+ function buildChartTransform() {
+ const displayTransform = new TimelineDisplayTransform();
+ displayTransform.panX = -20;
+ displayTransform.scaleX = 0.5;
+
+ const seriesYAxis = new ChartSeriesYAxis(-100, 100);
+
+ const chartTransform = new ChartTransform(
+ displayTransform,
+ seriesYAxis,
+ 500, /* trackWidth */
+ 80, /* trackHeight */
+ 15, /* topPadding */
+ 5, /* bottomPadding */
+ 3 /* pixelRatio */);
+
+ return chartTransform;
+ }
+
+ test('checkFields', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.pixelRatio, 3);
+
+ assert.strictEqual(t.leftViewX, 0);
+ assert.strictEqual(t.rightViewX, 500);
+ assert.strictEqual(t.leftTimestamp, 20);
+ assert.strictEqual(t.rightTimestamp, 1020);
+
+ assert.strictEqual(t.outerTopViewY, 0);
+ assert.strictEqual(t.innerTopViewY, 15);
+ assert.strictEqual(t.innerBottomViewY, 75);
+ assert.strictEqual(t.outerBottomViewY, 80);
+ });
+
+ test('checkWorldXToViewX', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.worldXToViewX(-100), -60);
+ assert.strictEqual(t.worldXToViewX(0), -10);
+ assert.strictEqual(t.worldXToViewX(520), 250);
+ assert.strictEqual(t.worldXToViewX(1020), 500);
+ assert.strictEqual(t.worldXToViewX(1200), 590);
+ });
+
+ test('checkViewXToWorldX', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.viewXToWorldX(-60), -100);
+ assert.strictEqual(t.viewXToWorldX(-10), 0);
+ assert.strictEqual(t.viewXToWorldX(250), 520);
+ assert.strictEqual(t.viewXToWorldX(500), 1020);
+ assert.strictEqual(t.viewXToWorldX(590), 1200);
+ });
+
+ test('checkWorldYToViewY', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.worldYToViewY(-200), 105);
+ assert.strictEqual(t.worldYToViewY(-100), 75);
+ assert.strictEqual(t.worldYToViewY(0), 45);
+ assert.strictEqual(t.worldYToViewY(100), 15);
+ assert.strictEqual(t.worldYToViewY(200), -15);
+ });
+
+ test('checkViewYToWorldY', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.viewYToWorldY(105), -200);
+ assert.strictEqual(t.viewYToWorldY(75), -100);
+ assert.strictEqual(t.viewYToWorldY(45), 0);
+ assert.strictEqual(t.viewYToWorldY(15), 100);
+ assert.strictEqual(t.viewYToWorldY(-15), 200);
+ });
+
+ test('checkVectorToWorldDistance', function() {
+ const t = buildChartTransform();
+
+ assert.strictEqual(t.vectorToWorldDistance(105), 350);
+ assert.strictEqual(t.vectorToWorldDistance(75), 250);
+ assert.strictEqual(t.vectorToWorldDistance(45), 150);
+ assert.strictEqual(t.vectorToWorldDistance(15), 50);
+ assert.strictEqual(t.vectorToWorldDistance(-15), 50);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html
new file mode 100644
index 00000000000..ecaac0dd3b1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_to_track_map.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * ContainerToTrackMap is a class to handle building and accessing a map
+ * between an EventContainer's stableId and its handling track.
+ *
+ * @constructor
+ */
+ function ContainerToTrackMap() {
+ this.stableIdToTrackMap_ = {};
+ }
+
+ ContainerToTrackMap.prototype = {
+ addContainer(container, track) {
+ if (!track) {
+ throw new Error('Must provide a track.');
+ }
+ this.stableIdToTrackMap_[container.stableId] = track;
+ },
+
+ clear() {
+ this.stableIdToTrackMap_ = {};
+ },
+
+ getTrackByStableId(stableId) {
+ return this.stableIdToTrackMap_[stableId];
+ }
+ };
+
+ return {
+ ContainerToTrackMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html
new file mode 100644
index 00000000000..454c1df585c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/container_track.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/task.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const Task = tr.b.Task;
+
+ /**
+ * A generic track that contains other tracks as its children.
+ * @constructor
+ */
+ const ContainerTrack = tr.ui.b.define('container-track', tr.ui.tracks.Track);
+ ContainerTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ },
+
+ detach() {
+ Polymer.dom(this).textContent = '';
+ },
+
+ get tracks_() {
+ const tracks = [];
+ for (let i = 0; i < this.children.length; i++) {
+ if (this.children[i] instanceof tr.ui.tracks.Track) {
+ tracks.push(this.children[i]);
+ }
+ }
+ return tracks;
+ },
+
+ drawTrack(type) {
+ this.tracks_.forEach(function(track) {
+ track.drawTrack(type);
+ });
+ },
+
+ /**
+ * Adds items intersecting the given range to a selection.
+ * @param {number} loVX Lower X bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVX Upper X bound of the interval to search, in
+ * viewspace.
+ * @param {number} loY Lower Y bound of the interval to search, in
+ * viewspace space.
+ * @param {number} hiY Upper Y bound of the interval to search, in
+ * viewspace space.
+ * @param {Selection} selection Selection to which to add results.
+ */
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ const trackClientRect = this.tracks_[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.tracks_[i].addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addIntersectingEventsInRangeToSelection.
+ apply(this, arguments);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ for (const track of this.tracks_) {
+ track.addEventsToTrackMap(eventToTrackMap);
+ }
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ this.tracks_[i].addAllEventsMatchingFilterToSelection(
+ filter, selection);
+ }
+ },
+
+ addAllEventsMatchingFilterToSelectionAsTask(filter, selection) {
+ const task = new Task();
+ for (let i = 0; i < this.tracks_.length; i++) {
+ task.subTask(function(i) {
+ return function() {
+ this.tracks_[i].addAllEventsMatchingFilterToSelection(
+ filter, selection);
+ };
+ }(i), this);
+ }
+ return task;
+ },
+
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ for (let i = 0; i < this.tracks_.length; i++) {
+ const trackClientRect = this.tracks_[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.tracks_[i].addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ this.tracks_.forEach(function(track) {
+ track.addContainersToTrackMap(containerToTrackMap);
+ });
+ },
+
+ clearTracks_() {
+ this.tracks_.forEach(function(track) {
+ Polymer.dom(this).removeChild(track);
+ }, this);
+ }
+ };
+
+ return {
+ ContainerTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html
new file mode 100644
index 00000000000..7f25e41bf6a
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a Counter object.
+ * @constructor
+ * @extends {ChartTrack}
+ */
+ const CounterTrack = tr.ui.b.define('counter-track', tr.ui.tracks.ChartTrack);
+
+ CounterTrack.prototype = {
+ __proto__: tr.ui.tracks.ChartTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ChartTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('counter-track');
+ },
+
+ get counter() {
+ return this.chart;
+ },
+
+ set counter(counter) {
+ this.heading = counter.name + ': ';
+ this.series = CounterTrack.buildChartSeriesFromCounter(counter);
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ getModelEventFromItem(chartValue) {
+ return chartValue;
+ }
+ };
+
+ CounterTrack.buildChartSeriesFromCounter = function(counter) {
+ const numSeries = counter.series.length;
+ const totals = counter.totals;
+
+ // Create one common axis for all series.
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+
+ // Build one chart series for each counter series.
+ const chartSeries = counter.series.map(function(series, seriesIndex) {
+ const chartPoints = series.samples.map(function(sample, sampleIndex) {
+ const total = totals[sampleIndex * numSeries + seriesIndex];
+ return new tr.ui.tracks.ChartPoint(sample, sample.timestamp, total);
+ });
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: series.color
+ };
+ return new tr.ui.tracks.ChartSeries(
+ chartPoints, seriesYAxis, renderingConfig);
+ });
+
+ // Show the first series (with the smallest cumulative value) at the top.
+ chartSeries.reverse();
+
+ return chartSeries;
+ };
+
+ return {
+ CounterTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html
new file mode 100644
index 00000000000..3a4f84a14b4
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_perf_test.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ function getSynchronous(url) {
+ const req = new XMLHttpRequest();
+ req.open('GET', url, false);
+ // Without the mime type specified like this, the file's bytes are not
+ // retrieved correctly.
+ req.overrideMimeType('text/plain; charset=x-user-defined');
+ req.send(null);
+ return req.responseText;
+ }
+
+ const ZOOM_STEPS = 10;
+ const ZOOM_COEFFICIENT = 1.2;
+
+ let model = undefined;
+
+ let drawingContainer;
+ let viewportDiv;
+
+ let viewportWidth;
+ let worldMid;
+
+ let startScale = undefined;
+
+ function timedCounterTrackPerfTest(name, testFn, iterations) {
+ function setUpOnce() {
+ if (model !== undefined) return;
+
+ const fileUrl = '/test_data/counter_tracks.html';
+ const events = getSynchronous(fileUrl);
+ model = tr.c.TestUtils.newModelWithEvents([events]);
+ }
+
+ function setUp() {
+ setUpOnce();
+ viewportDiv = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(viewportDiv);
+
+ drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ viewport.modelTrackContainer = drawingContainer;
+
+ const modelTrack = new tr.ui.tracks.ModelTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(modelTrack);
+
+ modelTrack.model = model;
+
+ Polymer.dom(viewportDiv).appendChild(drawingContainer);
+
+ this.addHTMLOutput(viewportDiv);
+
+ // Size the canvas.
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ // Size the viewport.
+ viewportWidth = drawingContainer.canvas.width;
+ const min = model.bounds.min;
+ const range = model.bounds.range;
+ worldMid = min + range / 2;
+
+ const boost = range * 0.15;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(min - boost, min + range + boost, viewportWidth);
+ modelTrack.viewport.setDisplayTransformImmediately(dt);
+ startScale = dt.scaleX;
+
+ // Select half of the counter samples.
+ for (const pid in model.processes) {
+ const counters = model.processes[pid].counters;
+ for (const cid in counters) {
+ const series = counters[cid].series;
+ for (let i = 0; i < series.length; i++) {
+ const samples = series[i].samples;
+ for (let j = Math.floor(samples.length / 2); j < samples.length;
+ j++) {
+ samples[j].selectionState =
+ tr.model.SelectionState.SELECTED;
+ }
+ }
+ }
+ }
+ }
+
+ function tearDown() {
+ viewportDiv.innerText = '';
+ drawingContainer = undefined;
+ }
+
+ timedPerfTest(name, testFn, {
+ setUp,
+ tearDown,
+ iterations
+ });
+ }
+
+ const n110100 = [1, 10, 100];
+ n110100.forEach(function(val) {
+ timedCounterTrackPerfTest(
+ 'draw_softwareCanvas_' + val,
+ function() {
+ let scale = startScale;
+ for (let i = 0; i < ZOOM_STEPS; i++) {
+ const dt =
+ drawingContainer.viewport.currentDisplayTransform.clone();
+ scale *= ZOOM_COEFFICIENT;
+ dt.scaleX = scale;
+ dt.xPanWorldPosToViewPos(worldMid, 'center', viewportWidth);
+ drawingContainer.viewport.setDisplayTransformImmediately(dt);
+ drawingContainer.draw_();
+ }
+ }, val);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html
new file mode 100644
index 00000000000..dd0286b6b67
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/counter_track_test.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const Counter = tr.model.Counter;
+ const Viewport = tr.ui.TimelineViewport;
+ const CounterTrack = tr.ui.tracks.CounterTrack;
+
+ const runTest = function(timestamps, samples, testFn) {
+ const testEl = document.createElement('div');
+
+ const ctr = new Counter(undefined, 'foo', '', 'foo');
+ const n = samples.length;
+
+ for (let i = 0; i < n; ++i) {
+ ctr.addSeries(new tr.model.CounterSeries('value' + i,
+ ColorScheme.getColorIdForGeneralPurposeString('value' + i)));
+ }
+
+ for (let i = 0; i < samples.length; ++i) {
+ for (let k = 0; k < timestamps.length; ++k) {
+ ctr.series[i].addCounterSample(timestamps[k], samples[i][k]);
+ }
+ }
+
+ ctr.updateBounds();
+
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new CounterTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ // Force the container to update sizes so the test can use coordinates that
+ // make sense. This has to be after the adding of the track as we need to
+ // use the track header to figure out our positioning.
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ track.heading = ctr.name;
+ track.counter = ctr;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 10, track.clientWidth * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ testFn(ctr, drawingContainer, track);
+ };
+
+ test('instantiate', function() {
+ const ctr = new Counter(undefined, 'testBasicCounter', '',
+ 'testBasicCounter');
+ ctr.addSeries(new tr.model.CounterSeries('value1',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ 'testBasicCounter.value1')));
+ ctr.addSeries(new tr.model.CounterSeries('value2',
+ ColorScheme.getColorIdForGeneralPurposeString(
+ 'testBasicCounter.value2')));
+
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 3, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+ for (let i = 0; i < samples.length; ++i) {
+ for (let k = 0; k < timestamps.length; ++k) {
+ ctr.series[i].addCounterSample(timestamps[k], samples[i][k]);
+ }
+ }
+
+ ctr.updateBounds();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new CounterTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = ctr.name;
+ track.counter = ctr;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 7.7, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('basicCounterXPointPicking', function() {
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 3, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+
+ runTest.call(this, timestamps, samples, function(ctr, container, track) {
+ const clientRect = track.getBoundingClientRect();
+ const y75 = clientRect.top + (0.75 * clientRect.height);
+
+ // In bounds.
+ let sel = new tr.model.EventSet();
+ let x = 0.15 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+
+ let nextSeriesIndex = 1;
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.series.counter, ctr);
+ assert.strictEqual(event.getSampleIndex(), 1);
+ assert.strictEqual(event.series.seriesIndex, nextSeriesIndex--);
+ }
+
+ // Outside bounds.
+ sel = new tr.model.EventSet();
+ x = -0.5 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+ assert.strictEqual(sel.length, 0);
+
+ sel = new tr.model.EventSet();
+ x = 0.8 * clientRect.width;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y75, y75 + 1, sel);
+ assert.strictEqual(sel.length, 0);
+ });
+ });
+
+ test('counterTrackAddClosestEventToSelection', function() {
+ const timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ const samples = [[0, 4, 1, 2, 3, 1, 3, 3.1],
+ [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+
+ runTest.call(this, timestamps, samples, function(ctr, container, track) {
+ // Before with not range.
+ let sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, 0, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Before with negative range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, -10, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Before first sample.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(-1, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 0);
+ }
+
+ // Between and closer to sample before.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(1.3, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 1);
+ }
+
+ // Between samples with bad range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(1.45, 0.25, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+
+ // Between and closer to next sample.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(4.7, 6, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 5);
+ }
+
+ // After last sample with good range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(8.5, 2, 0, 0, sel);
+ assert.strictEqual(sel.length, 2);
+ for (const event of sel) {
+ assert.strictEqual(event.getSampleIndex(), 7);
+ }
+
+ // After last sample with bad range.
+ sel = new tr.model.EventSet();
+ track.addClosestEventToSelection(10, 1, 0, 0, sel);
+ assert.strictEqual(sel.length, 0);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html
new file mode 100644
index 00000000000..3a6c627fb38
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * Visualizes a Cpu using a series of SliceTracks.
+ * @constructor
+ */
+ const CpuTrack =
+ tr.ui.b.define('cpu-track', tr.ui.tracks.ContainerTrack);
+ CpuTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('cpu-track');
+ this.detailedMode_ = true;
+ },
+
+ get cpu() {
+ return this.cpu_;
+ },
+
+ set cpu(cpu) {
+ this.cpu_ = cpu;
+ this.updateContents_();
+ },
+
+ get detailedMode() {
+ return this.detailedMode_;
+ },
+
+ set detailedMode(detailedMode) {
+ this.detailedMode_ = detailedMode;
+ this.updateContents_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(value) {
+ this.tooltip_ = value;
+ this.updateContents_();
+ },
+
+ get hasVisibleContent() {
+ if (this.cpu_ === undefined) return false;
+
+ const cpu = this.cpu_;
+ if (cpu.slices.length) return true;
+
+ if (cpu.samples && cpu.samples.length) return true;
+
+ if (Object.keys(cpu.counters).length > 0) return true;
+
+ return false;
+ },
+
+ updateContents_() {
+ this.detach();
+ if (!this.cpu_) return;
+
+ const slices = this.cpu_.slices;
+ if (slices.length) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ track.heading = this.cpu_.userFriendlyName + ':';
+ Polymer.dom(this).appendChild(track);
+ }
+
+ if (this.detailedMode_) {
+ this.appendSamplesTracks_();
+
+ for (const counterName in this.cpu_.counters) {
+ const counter = this.cpu_.counters[counterName];
+ const track = new tr.ui.tracks.CounterTrack(this.viewport);
+ track.heading = this.cpu_.userFriendlyName + ' ' +
+ counter.name + ':';
+ track.counter = counter;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+ },
+
+ appendSamplesTracks_() {
+ const samples = this.cpu_.samples;
+ if (samples === undefined || samples.length === 0) {
+ return;
+ }
+ const samplesByTitle = {};
+ samples.forEach(function(sample) {
+ if (samplesByTitle[sample.title] === undefined) {
+ samplesByTitle[sample.title] = [];
+ }
+ samplesByTitle[sample.title].push(sample);
+ });
+
+ const sampleTitles = Object.keys(samplesByTitle);
+ sampleTitles.sort();
+
+ sampleTitles.forEach(function(sampleTitle) {
+ const samples = samplesByTitle[sampleTitle];
+ const samplesTrack = new tr.ui.tracks.SliceTrack(this.viewport);
+ samplesTrack.group = this.cpu_;
+ samplesTrack.slices = samples;
+ samplesTrack.heading = this.cpu_.userFriendlyName + ': ' +
+ sampleTitle;
+ samplesTrack.tooltip = this.cpu_.userFriendlyDetails;
+ samplesTrack.selectionGenerator = function() {
+ const selection = new tr.model.EventSet();
+ for (let i = 0; i < samplesTrack.slices.length; i++) {
+ selection.push(samplesTrack.slices[i]);
+ }
+ return selection;
+ };
+ Polymer.dom(this).appendChild(samplesTrack);
+ }, this);
+ }
+ };
+
+ return {
+ CpuTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html
new file mode 100644
index 00000000000..442992522f5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_track_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Cpu = tr.model.Cpu;
+ const CpuTrack = tr.ui.tracks.CpuTrack;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const StackFrame = tr.model.StackFrame;
+ const Sample = tr.model.Sample;
+ const Thread = tr.model.Thread;
+ const Viewport = tr.ui.TimelineViewport;
+
+ test('basicCpu', function() {
+ const cpu = new Cpu({}, 7);
+ cpu.slices = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8)
+ ];
+ cpu.updateBounds();
+
+ const testEl = document.createElement('div');
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+
+ const track = new CpuTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.heading = 'CPU ' + cpu.cpuNumber;
+ track.cpu = cpu;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 11.1, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+
+ test('withSamples', function() {
+ let thread;
+ let cpu;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ cpu = model.kernel.getOrCreateCpu(1);
+ thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+
+ const nodeA = tr.c.TestUtils.newProfileNode(model, 'a');
+ const nodeB = tr.c.TestUtils.newProfileNode(model, 'b', nodeA);
+ const nodeC = tr.c.TestUtils.newProfileNode(model, 'c', nodeB);
+ const nodeD = tr.c.TestUtils.newProfileNode(model, 'd', nodeA);
+
+ model.samples.push(new Sample(10, 'instructions_retired', nodeC, thread,
+ undefined, 10));
+ model.samples.push(new Sample(20, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(30, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(40, 'instructions_retired', nodeD, thread,
+ undefined, 10));
+
+ model.samples.push(new Sample(25, 'page_fault', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(35, 'page_fault', nodeD, thread,
+ undefined, 10));
+ }
+ });
+
+ const testEl = document.createElement('div');
+ const viewport = new Viewport(testEl);
+
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+
+ const track = new CpuTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ track.heading = 'CPU ' + cpu.cpuNumber;
+ track.cpu = cpu;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 11.1, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html
new file mode 100644
index 00000000000..912220b8236
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<style>
+.cpu-usage-track {
+ height: 90px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+
+ /**
+ * A track that displays the cpu usage of a process.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.ChartTrack}
+ */
+ const CpuUsageTrack = tr.ui.b.define('cpu-usage-track', ChartTrack);
+
+ CpuUsageTrack.prototype = {
+ __proto__: ChartTrack.prototype,
+
+ decorate(viewport) {
+ ChartTrack.prototype.decorate.call(this, viewport);
+ this.classList.add('cpu-usage-track');
+ this.heading = 'CPU usage';
+ this.cpuUsageSeries_ = undefined;
+ },
+
+ // Given a tr.Model, it creates a cpu usage series and a graph.
+ initialize(model) {
+ if (model !== undefined) {
+ this.cpuUsageSeries_ = model.device.cpuUsageSeries;
+ } else {
+ this.cpuUsageSeries_ = undefined;
+ }
+ this.series = this.buildChartSeries_();
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ get hasVisibleContent() {
+ return !!this.cpuUsageSeries_ &&
+ this.cpuUsageSeries_.samples.length > 0;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ containerToTrackMap.addContainer(this.series_, this);
+ },
+
+ buildChartSeries_(yAxis, color) {
+ if (!this.hasVisibleContent) return [];
+
+ yAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+ const usageSamples = this.cpuUsageSeries_.samples;
+ const pts = new Array(usageSamples.length + 1);
+ for (let i = 0; i < usageSamples.length; i++) {
+ pts[i] = new tr.ui.tracks.ChartPoint(undefined,
+ usageSamples[i].start, usageSamples[i].usage);
+ }
+ pts[usageSamples.length] = new tr.ui.tracks.ChartPoint(undefined,
+ usageSamples[usageSamples.length - 1].start, 0);
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: color
+ };
+
+ return [new tr.ui.tracks.ChartSeries(pts, yAxis, renderingConfig)];
+ },
+ };
+
+ return {
+ CpuUsageTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html
new file mode 100644
index 00000000000..2970e81eaf8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/cpu_usage_track_test.html
@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/cpu/cpu_usage_auditor.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel='import' href='/tracing/ui/base/constants.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel="import" href="/tracing/ui/tracks/cpu_usage_track.html">
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const DIFF_EPSILON = 0.0001;
+
+ // Input : slices is an array-of-array-of slices. Each top level array
+ // represents a process. So, each slice in one of the top level array
+ // will be placed in the same process.
+ function buildModel(slices) {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < slices.length; i++) {
+ const thread = process.getOrCreateThread(i);
+ slices[i].forEach(s => thread.sliceGroup.pushSlice(s));
+ }
+ });
+ const auditor = new tr.e.audits.CpuUsageAuditor(model);
+ auditor.runAnnotate();
+ return model;
+ }
+
+ // Compare float arrays based on an epsilon since floating point arithmetic
+ // is not always 100% accurate.
+ function assertArrayValuesCloseTo(actualValue, expectedValue) {
+ assert.lengthOf(actualValue, expectedValue.length);
+ for (let i = 0; i < expectedValue.length; i++) {
+ assert.closeTo(actualValue[i], expectedValue[i], DIFF_EPSILON);
+ }
+ }
+
+ function createCpuUsageTrack(model, interval) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ div.appendChild(drawingContainer);
+ const track = new tr.ui.tracks.CpuUsageTrack(drawingContainer.viewport);
+ if (model !== undefined) {
+ setDisplayTransformFromBounds(viewport, model.bounds);
+ }
+ track.initialize(model, interval);
+ drawingContainer.appendChild(track);
+ this.addHTMLOutput(drawingContainer);
+ return track;
+ }
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ function setDisplayTransformFromBounds(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ }
+
+ test('computeCpuUsage_simple', function() {
+ // Set the boundaries, from 0-15 ms. This slice will not
+ // contain any CPU usage data, it's just to make the boundaries
+ // of the bins go as 0-1, 1-2, 2-3, etc. This also tests whether
+ // this function works properly in the presence of slices that
+ // don't include CPU usage data.
+ const bigSlice = new tr.model.ThreadSlice('', title, 0, 0, {}, 15);
+ // First thread.
+ // 0 5 10 15
+ // [ sliceA ]
+ // [ sliceB ] [C ]
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 0.5, {}, 5);
+ sliceA.cpuDuration = 5;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 2.5, {}, 8);
+ sliceB.cpuDuration = 6;
+ // The slice completely fits into an interval and is the last.
+ const sliceC = new tr.model.ThreadSlice('', title, 0, 12.5, {}, 2);
+ sliceC.cpuDuration = 1;
+
+ // Second thread.
+ // 0 5 10 15
+ // [ sliceD ][ sliceE ]
+ const sliceD = new tr.model.ThreadSlice('', title, 0, 3.5, {}, 3);
+ sliceD.cpuDuration = 3;
+ const sliceE = new tr.model.ThreadSlice('', title, 0, 6.5, {}, 6);
+ sliceE.cpuDuration = 3;
+
+ const model = buildModel([
+ [bigSlice, sliceA, sliceB, sliceC],
+ [sliceD, sliceE]
+ ]);
+
+ // Compute average CPU usage over A (but not over B and C).
+ const avgCpuUsageA = sliceA.cpuSelfTime / sliceA.selfTime;
+ // Compute average CPU usage over B, C, D, E. They don't have subslices.
+ const avgCpuUsageB = sliceB.cpuDuration / sliceB.duration;
+ const avgCpuUsageC = sliceC.cpuDuration / sliceC.duration;
+ const avgCpuUsageD = sliceD.cpuDuration / sliceD.duration;
+ const avgCpuUsageE = sliceE.cpuDuration / sliceE.duration;
+
+ const expectedValue = [
+ 0,
+ avgCpuUsageA,
+ avgCpuUsageA,
+ avgCpuUsageA + avgCpuUsageB,
+ avgCpuUsageA + avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageA + avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageB + avgCpuUsageD,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageB + avgCpuUsageE,
+ avgCpuUsageE,
+ avgCpuUsageE,
+ avgCpuUsageC,
+ avgCpuUsageC,
+ 0
+ ];
+ const track = createCpuUsageTrack.call(this, model);
+ const actualValue = track.series[0].points.map(point => point.y);
+ assertArrayValuesCloseTo(actualValue, expectedValue);
+ });
+
+ test('computeCpuUsage_longDurationThreadSlice', function() {
+ // Create a slice covering 24 hours.
+ const sliceA = new tr.model.ThreadSlice(
+ '', title, 0, 0, {}, 24 * 60 * 60 * 1000);
+ sliceA.cpuDuration = sliceA.duration * 0.25;
+
+ const model = buildModel([[sliceA]]);
+
+ const track = createCpuUsageTrack.call(this, model);
+ const cpuSamples = track.series[0].points.map(point => point.y);
+
+ // All except the last sample is 0.25, since sliceA.cpuDuration was set to
+ // 0.25 of the total.
+ for (const cpuSample of cpuSamples.slice(0, cpuSamples.length - 1)) {
+ assert.closeTo(cpuSample, 0.25, DIFF_EPSILON);
+ }
+ // The last sample is 0.
+ assert.closeTo(cpuSamples[cpuSamples.length - 1], 0, DIFF_EPSILON);
+ });
+
+ test('instantiate', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 5.5111, {}, 47.1023);
+ sliceA.cpuDuration = 25;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 11.2384, {}, 1.8769);
+ sliceB.cpuDuration = 1.5;
+ const sliceC = new tr.model.ThreadSlice('', title, 0, 11.239, {}, 5.8769);
+ sliceC.cpuDuration = 5;
+ const sliceD = new tr.model.ThreadSlice('', title, 0, 48.012, {}, 5.01);
+ sliceD.cpuDuration = 4;
+
+ const model = buildModel([[sliceA, sliceB, sliceC, sliceD]]);
+ createCpuUsageTrack.call(this, model);
+ });
+
+ test('hasVisibleContent_trueWithThreadSlicePresent', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 48.012, {}, 5.01);
+ sliceA.cpuDuration = 4;
+ const model = buildModel([[sliceA]]);
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedProcessModel', function() {
+ const track = createCpuUsageTrack.call(this, undefined);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithNoThreadSlice', function() {
+ // model with a CPU and a thread but no ThreadSlice.
+ const model = buildModel([]);
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_trueWithSubSlices', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 5.5111, {}, 47.1023);
+ sliceA.cpuDuration = 25;
+ const sliceB = new tr.model.ThreadSlice('', title, 0, 11.2384, {}, 1.8769);
+ sliceB.cpuDuration = 1.5;
+
+ const model = buildModel([[sliceA, sliceB]]);
+ const process = model.getProcess(1);
+ // B will become lowest level slices of A.
+ process.getThread(0).sliceGroup.createSubSlices();
+ assert.strictEqual(
+ sliceA.cpuSelfTime, (sliceA.cpuDuration - sliceB.cpuDuration));
+ const track = createCpuUsageTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html
new file mode 100644
index 00000000000..a068a7ebebb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/power_series_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ContainerTrack = tr.ui.tracks.ContainerTrack;
+
+ // TODO(charliea): Make this track collapsible.
+ /**
+ * Track to visualize the device model.
+ *
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const DeviceTrack = tr.ui.b.define('device-track', ContainerTrack);
+
+ DeviceTrack.prototype = {
+
+ __proto__: ContainerTrack.prototype,
+
+ decorate(viewport) {
+ ContainerTrack.prototype.decorate.call(this, viewport);
+
+ Polymer.dom(this).classList.add('device-track');
+ this.device_ = undefined;
+ this.powerSeriesTrack_ = undefined;
+ },
+
+ get device() {
+ return this.device_;
+ },
+
+ set device(device) {
+ this.device_ = device;
+ this.updateContents_();
+ },
+
+ get powerSeriesTrack() {
+ return this.powerSeriesTrack_;
+ },
+
+ get hasVisibleContent() {
+ return (this.powerSeriesTrack_ &&
+ this.powerSeriesTrack_.hasVisibleContent);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ContainerTrack.prototype.addContainersToTrackMap.call(
+ this, containerToTrackMap);
+ containerToTrackMap.addContainer(this.device, this);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ this.tracks_.forEach(function(track) {
+ track.addEventsToTrackMap(eventToTrackMap);
+ });
+ },
+
+ appendPowerSeriesTrack_() {
+ this.powerSeriesTrack_ = new tr.ui.tracks.PowerSeriesTrack(this.viewport);
+ this.powerSeriesTrack_.powerSeries = this.device.powerSeries;
+
+ if (this.powerSeriesTrack_.hasVisibleContent) {
+ Polymer.dom(this).appendChild(this.powerSeriesTrack_);
+ Polymer.dom(this).appendChild(
+ new tr.ui.tracks.SpacingTrack(this.viewport));
+ }
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+ this.appendPowerSeriesTrack_();
+ }
+ };
+
+ return {
+ DeviceTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html
new file mode 100644
index 00000000000..fdd3b392993
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/device_track_test.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel='import' href='/tracing/model/device.html'>
+<link rel='import' href='/tracing/model/model.html'>
+<link rel="import" href="/tracing/ui/base/constants.html">
+<link rel='import' href='/tracing/ui/timeline_display_transform.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel='import' href='/tracing/ui/tracks/device_track.html'>
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+<link rel='import' href='/tracing/ui/tracks/event_to_track_map.html'>
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Device = tr.model.Device;
+ const DeviceTrack = tr.ui.tracks.DeviceTrack;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+
+ const createDrawingContainer = function(series) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ if (series) {
+ series.updateBounds();
+ setDisplayTransformFromBounds(viewport, series.bounds);
+ }
+
+ return drawingContainer;
+ };
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ const setDisplayTransformFromBounds = function(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ };
+
+ test('instantiate', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+ device.powerSeries.addPowerSample(1, 3);
+ device.powerSeries.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+ });
+
+ test('instantiate_noPowerSeries', function() {
+ const device = new Device(new Model());
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ // Adding output should throw due to no visible content.
+ assert.throw(function() { this.addHTMLOutput(drawingContainer); });
+ });
+
+ test('setDevice_clearsTrackBeforeUpdating', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+ device.powerSeries.addPowerSample(1, 3);
+ device.powerSeries.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(device.powerSeries);
+
+ // Set the device twice and make sure that this doesn't result in
+ // the track appearing twice.
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+ track.device = device;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+
+ // The device track should still have two subtracks: one counter track and
+ // one spacing track.
+ assert.strictEqual(track.tracks_.length, 2);
+ });
+
+ test('addContainersToTrackMap', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+
+ const drawingContainer = createDrawingContainer(device.series);
+ const track = new DeviceTrack(drawingContainer.viewport);
+ track.device = device;
+
+ const containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
+ track.addContainersToTrackMap(containerToTrackMap);
+
+ assert.strictEqual(containerToTrackMap.getTrackByStableId('Device'), track);
+ assert.strictEqual(
+ containerToTrackMap.getTrackByStableId('Device.PowerSeries'),
+ track.powerSeriesTrack);
+ });
+
+ test('addEventsToTrackMap', function() {
+ const device = new Device(new Model());
+ device.powerSeries = new PowerSeries(device);
+ device.powerSeries.addPowerSample(0, 1);
+ device.powerSeries.addPowerSample(0.5, 2);
+
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new DeviceTrack(viewport);
+ track.device = device;
+
+ const eventToTrackMap = new tr.ui.tracks.EventToTrackMap();
+ track.addEventsToTrackMap(eventToTrackMap);
+
+ const expected = new tr.ui.tracks.EventToTrackMap();
+ expected[device.powerSeries.samples[0].guid] = track.powerSeriesTrack;
+ expected[device.powerSeries.samples[1].guid] = track.powerSeriesTrack;
+
+ assert.deepEqual(eventToTrackMap, expected);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css
new file mode 100644
index 00000000000..a8f4d17c91c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.css
@@ -0,0 +1,18 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.drawing-container {
+ display: inline;
+ overflow: auto;
+ overflow-x: hidden;
+ position: relative;
+}
+
+.drawing-container-canvas {
+ display: block;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html
new file mode 100644
index 00000000000..d13a3a5383c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/drawing_container.css">
+
+<link rel="import" href="/tracing/base/raf.html">
+<link rel="import" href="/tracing/ui/base/constants.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const DrawType = {
+ GENERAL_EVENT: 1,
+ INSTANT_EVENT: 2,
+ BACKGROUND: 3,
+ GRID: 4,
+ FLOW_ARROWS: 5,
+ MARKERS: 6,
+ HIGHLIGHTS: 7,
+ ANNOTATIONS: 8
+ };
+
+ // Must be > 1.0. This is the maximum multiple by which the size
+ // of the canvas can exceed the window dimensions. For example
+ // if window.innerHeight is 1000 and this is 1.4, then the
+ // largest the canvas height can be set to is 1400px assuming a
+ // window.devicePixelRatio of 1.
+ // Currently this value is set rather large to mostly match
+ // previous behavior & performance. This should be reduced to
+ // be as small as possible once raw drawing performance is improved
+ // such that a repaint doesn't incur a large jank
+ const MAX_OVERSIZE_MULTIPLE = 3.0;
+ const REDRAW_SLOP = (MAX_OVERSIZE_MULTIPLE - 1) / 2;
+
+ const DrawingContainer = tr.ui.b.define('drawing-container',
+ tr.ui.tracks.Track);
+
+ DrawingContainer.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('drawing-container');
+
+ this.canvas_ = document.createElement('canvas');
+ this.canvas_.className = 'drawing-container-canvas';
+ this.canvas_.style.left = tr.ui.b.constants.HEADING_WIDTH + 'px';
+ Polymer.dom(this).appendChild(this.canvas_);
+
+ this.ctx_ = this.canvas_.getContext('2d');
+ this.offsetY_ = 0;
+
+ this.viewportChange_ = this.viewportChange_.bind(this);
+ this.viewport.addEventListener('change', this.viewportChange_);
+
+ window.addEventListener('resize', this.windowResized_.bind(this));
+ this.addEventListener('scroll', this.scrollChanged_.bind(this));
+ },
+
+ // Needed to support the calls in TimelineTrackView.
+ get canvas() {
+ return this.canvas_;
+ },
+
+ context() {
+ return this.ctx_;
+ },
+
+ viewportChange_() {
+ this.invalidate();
+ },
+
+ windowResized_() {
+ this.invalidate();
+ },
+
+ scrollChanged_() {
+ if (this.updateOffsetY_()) {
+ this.invalidate();
+ }
+ },
+
+ invalidate() {
+ if (this.rafPending_) return;
+
+ this.rafPending_ = true;
+
+ tr.b.requestPreAnimationFrame(this.preDraw_, this);
+ },
+
+ preDraw_() {
+ this.rafPending_ = false;
+ this.updateCanvasSizeIfNeeded_();
+
+ tr.b.requestAnimationFrameInThisFrameIfPossible(this.draw_, this);
+ },
+
+ draw_() {
+ this.ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+
+ const typesToDraw = [
+ DrawType.BACKGROUND,
+ DrawType.HIGHLIGHTS,
+ DrawType.GRID,
+ DrawType.INSTANT_EVENT,
+ DrawType.GENERAL_EVENT,
+ DrawType.MARKERS,
+ DrawType.ANNOTATIONS,
+ DrawType.FLOW_ARROWS
+ ];
+
+ for (const idx in typesToDraw) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ this.children[i].drawTrack(typesToDraw[idx]);
+ }
+ }
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.canvas_.getBoundingClientRect();
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(
+ bounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ this.viewport.drawGridLines(
+ this.ctx_, viewLWorld, viewRWorld, viewHeight);
+ },
+
+ // Update's this.offsetY_, returning true if the value has changed
+ // and thus a redraw is needed, or false if it did not change.
+ updateOffsetY_() {
+ const maxYDelta = window.innerHeight * REDRAW_SLOP;
+ let newOffset = this.scrollTop - maxYDelta;
+ if (Math.abs(newOffset - this.offsetY_) <= maxYDelta) return false;
+ // Now clamp to the valid range.
+ const maxOffset = this.scrollHeight -
+ this.canvas_.getBoundingClientRect().height;
+ newOffset = Math.max(0, Math.min(newOffset, maxOffset));
+ if (newOffset !== this.offsetY_) {
+ this.offsetY_ = newOffset;
+ return true;
+ }
+ return false;
+ },
+
+ updateCanvasSizeIfNeeded_() {
+ const visibleChildTracks =
+ Array.from(this.children).filter(this.visibleFilter_);
+
+ if (visibleChildTracks.length === 0) {
+ return;
+ }
+
+ const thisBounds = this.getBoundingClientRect();
+
+ const firstChildTrackBounds =
+ visibleChildTracks[0].getBoundingClientRect();
+ const lastChildTrackBounds =
+ visibleChildTracks[visibleChildTracks.length - 1].
+ getBoundingClientRect();
+
+ const innerWidth = firstChildTrackBounds.width -
+ tr.ui.b.constants.HEADING_WIDTH;
+ const innerHeight = Math.min(
+ lastChildTrackBounds.bottom - firstChildTrackBounds.top,
+ Math.floor(window.innerHeight * MAX_OVERSIZE_MULTIPLE));
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ if (this.canvas_.width !== innerWidth * pixelRatio) {
+ this.canvas_.width = innerWidth * pixelRatio;
+ this.canvas_.style.width = innerWidth + 'px';
+ }
+
+ if (this.canvas_.height !== innerHeight * pixelRatio) {
+ this.canvas_.height = innerHeight * pixelRatio;
+ this.canvas_.style.height = innerHeight + 'px';
+ }
+
+ if (this.canvas_.top !== this.offsetY_) {
+ this.canvas_.top = this.offsetY_;
+ this.canvas_.style.top = this.offsetY_ + 'px';
+ }
+ },
+
+ visibleFilter_(element) {
+ if (!(element instanceof tr.ui.tracks.Track)) return false;
+
+ return window.getComputedStyle(element).display !== 'none';
+ },
+
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ const trackClientRect = this.children[i].getBoundingClientRect();
+ const a = Math.max(loY, trackClientRect.top);
+ const b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ this.children[i].addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection);
+ }
+ }
+
+ tr.ui.tracks.Track.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track)) {
+ continue;
+ }
+ this.children[i].addEventsToTrackMap(eventToTrackMap);
+ }
+ }
+ };
+
+ return {
+ DrawingContainer,
+ DrawType,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html
new file mode 100644
index 00000000000..7b778b1c332
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/drawing_container_perf_test.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/xhr.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/ui/extras/full_config.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ let generalModel;
+ function getOrCreateGeneralModel() {
+ if (generalModel !== undefined) {
+ generalModel;
+ }
+ const fileUrl = '/test_data/thread_time_visualisation.json.gz';
+ const events = tr.b.getSync(fileUrl);
+ generalModel = tr.c.TestUtils.newModelWithEvents([events]);
+ return generalModel;
+ }
+
+ function DCPerfTestCase(testName, opt_options) {
+ tr.b.unittest.PerfTestCase.call(this, testName, undefined, opt_options);
+ this.viewportDiv = undefined;
+ this.drawingContainer = undefined;
+ this.viewport = undefined;
+ }
+ DCPerfTestCase.prototype = {
+ __proto__: tr.b.unittest.PerfTestCase.prototype,
+
+ setUp(model) {
+ this.viewportDiv = document.createElement('div');
+
+ this.viewport = new tr.ui.TimelineViewport(this.viewportDiv);
+
+ this.drawingContainer = new tr.ui.tracks.DrawingContainer(this.viewport);
+ this.viewport.modelTrackContainer = this.drawingContainer;
+
+ const modelTrack = new tr.ui.tracks.ModelTrack(this.viewport);
+ Polymer.dom(this.drawingContainer).appendChild(modelTrack);
+
+ modelTrack.model = model;
+
+ Polymer.dom(this.viewportDiv).appendChild(this.drawingContainer);
+
+ this.addHTMLOutput(this.viewportDiv);
+
+ // Size the canvas.
+ this.drawingContainer.updateCanvasSizeIfNeeded_();
+
+ // Size the viewport.
+ const w = this.drawingContainer.canvas.width;
+ const min = model.bounds.min;
+ const range = model.bounds.range;
+
+ const boost = range * 0.15;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(min - boost, min + range + boost, w);
+ this.viewport.setDisplayTransformImmediately(dt);
+ },
+
+ runOneIteration() {
+ this.drawingContainer.draw_();
+ }
+ };
+
+
+ function GeneralDCPerfTestCase(testName, opt_options) {
+ DCPerfTestCase.call(this, testName, opt_options);
+ }
+
+ GeneralDCPerfTestCase.prototype = {
+ __proto__: DCPerfTestCase.prototype,
+
+ setUp() {
+ const model = getOrCreateGeneralModel();
+ DCPerfTestCase.prototype.setUp.call(this, model);
+ }
+ };
+
+ // Failing on Chrome canary, see
+ // https://github.com/catapult-project/catapult/issues/1826
+ flakyTest(new GeneralDCPerfTestCase('draw_softwareCanvas_One',
+ {iterations: 1}));
+ // Failing on Chrome stable on Windows, see
+ // https://github.com/catapult-project/catapult/issues/1908
+ flakyTest(new GeneralDCPerfTestCase('draw_softwareCanvas_Ten',
+ {iterations: 10}));
+ test(new GeneralDCPerfTestCase('draw_softwareCanvas_AHundred',
+ {iterations: 100}));
+
+ function AsyncDCPerfTestCase(testName, opt_options) {
+ DCPerfTestCase.call(this, testName, opt_options);
+ }
+
+ AsyncDCPerfTestCase.prototype = {
+ __proto__: DCPerfTestCase.prototype,
+
+ setUp() {
+ const model = tr.c.TestUtils.newModel(function(m) {
+ const proc = m.getOrCreateProcess(1);
+ for (let tid = 1; tid <= 5; tid++) {
+ const thread = proc.getOrCreateThread(tid);
+ for (let i = 0; i < 5000; i++) {
+ const mod = Math.floor(i / 100) % 4;
+ const slice = tr.c.TestUtils.newAsyncSliceEx({
+ name: 'Test' + i,
+ colorId: tid + mod,
+ id: tr.b.GUID.allocateSimple(),
+ start: i * 10,
+ duration: 9,
+ isTopLevel: true
+ });
+ thread.asyncSliceGroup.push(slice);
+ }
+ }
+ });
+ DCPerfTestCase.prototype.setUp.call(this, model);
+
+ const w = this.drawingContainer.canvas.width;
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(-2000, 54000, w);
+ this.viewport.setDisplayTransformImmediately(dt);
+ }
+ };
+ test(new AsyncDCPerfTestCase('draw_asyncSliceHeavy_Twenty',
+ {iterations: 20}));
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html
new file mode 100644
index 00000000000..f8ba209d01b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/event_to_track_map.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * EventToTrackMap provides a mapping mechanism between events and the
+ * tracks those events belong on.
+ * @constructor
+ */
+ function EventToTrackMap() {}
+
+ EventToTrackMap.prototype = {
+ addEvent(event, track) {
+ if (!track) {
+ throw new Error('Must provide a track.');
+ }
+ this[event.guid] = track;
+ }
+ };
+
+ return {
+ EventToTrackMap,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html
new file mode 100644
index 00000000000..3e8de1d9831
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const startCompare = function(x, y) { return x.start - y.start; };
+
+ /**
+ * Track enabling quick selection of frame slices/events.
+ * @constructor
+ */
+ const FrameTrack = tr.ui.b.define(
+ 'frame-track', tr.ui.tracks.LetterDotTrack);
+
+ FrameTrack.prototype = {
+ __proto__: tr.ui.tracks.LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Frames';
+
+ this.frames_ = undefined;
+ this.items = undefined;
+ },
+
+ get frames() {
+ return this.frames_;
+ },
+
+ set frames(frames) {
+ this.frames_ = frames;
+ if (frames === undefined) return;
+
+ this.frames_ = this.frames_.slice();
+ this.frames_.sort(startCompare);
+
+ // letter dots
+ this.items = this.frames_.map(function(frame) {
+ return new FrameDot(frame);
+ });
+ }
+ };
+
+ /**
+ * @constructor
+ * @extends {LetterDot}
+ */
+ function FrameDot(frame) {
+ tr.ui.tracks.LetterDot.call(this, frame, 'F', frame.colorId, frame.start);
+ }
+
+ FrameDot.prototype = {
+ __proto__: tr.ui.tracks.LetterDot.prototype
+ };
+
+ return {
+ FrameTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html
new file mode 100644
index 00000000000..94189d0fecb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/frame_track_test.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/frame.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/frame_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Frame = tr.model.Frame;
+ const FrameTrack = tr.ui.tracks.FrameTrack;
+ const EventSet = tr.model.EventSet;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createFrames = function() {
+ let frames = undefined;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(1);
+ for (let i = 1; i < 5; i++) {
+ const slice = tr.c.TestUtils.newSliceEx(
+ {title: 'work for frame', start: i * 20, duration: 10});
+ thread.sliceGroup.pushSlice(slice);
+ const events = [slice];
+ const threadTimeRanges =
+ [{thread, start: slice.start, end: slice.end}];
+ process.frames.push(new Frame(events, threadTimeRanges));
+ }
+ frames = process.frames;
+ });
+ return frames;
+ };
+
+ test('instantiate', function() {
+ const frames = createFrames();
+ frames[1].selectionState = SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = FrameTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.frames = frames;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.strictEqual(track.items[0].start, 20);
+ });
+
+ test('modelMapping', function() {
+ const frames = createFrames();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = FrameTrack(viewport);
+ track.frames = frames;
+
+ const a0 = track.items[0].modelItem;
+ assert.strictEqual(a0, frames[0]);
+ });
+
+ test('selectionMapping', function() {
+ const frames = createFrames();
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const track = FrameTrack(viewport);
+ track.frames = frames;
+
+ const selection = new EventSet();
+ track.items[0].addToSelection(selection);
+
+ // select both frame, but not its component slice
+ assert.strictEqual(selection.length, 1);
+
+ let frameCount = 0;
+ let eventCount = 0;
+ selection.forEach(function(event) {
+ if (event instanceof Frame) {
+ assert.strictEqual(event, frames[0]);
+ frameCount++;
+ } else {
+ eventCount++;
+ }
+ });
+ assert.strictEqual(frameCount, 1);
+ assert.strictEqual(eventCount, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html
new file mode 100644
index 00000000000..aaf9bc0a80d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const USED_MEMORY_TRACK_HEIGHT = 50;
+ const ALLOCATED_MEMORY_TRACK_HEIGHT = 50;
+
+ /**
+ * A track that displays an array of GlobalMemoryDump objects.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const GlobalMemoryDumpTrack = tr.ui.b.define(
+ 'global-memory-dump-track', tr.ui.tracks.ContainerTrack);
+
+ GlobalMemoryDumpTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.memoryDumps_ = undefined;
+ },
+
+ get memoryDumps() {
+ return this.memoryDumps_;
+ },
+
+ set memoryDumps(memoryDumps) {
+ this.memoryDumps_ = memoryDumps;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ // Show no tracks if there are no dumps.
+ if (!this.memoryDumps_ || !this.memoryDumps_.length) return;
+
+ this.appendDumpDotsTrack_();
+ this.appendUsedMemoryTrack_();
+ this.appendAllocatedMemoryTrack_();
+ },
+
+ appendDumpDotsTrack_() {
+ const items = tr.ui.tracks.buildMemoryLetterDots(this.memoryDumps_);
+ if (!items) return;
+
+ const track = new tr.ui.tracks.LetterDotTrack(this.viewport);
+ track.heading = 'Memory Dumps';
+ track.items = items;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendUsedMemoryTrack_() {
+ const tracks = [];
+ const perProcessSeries =
+ tr.ui.tracks.buildGlobalUsedMemoryChartSeries(this.memoryDumps_);
+ if (perProcessSeries !== undefined) {
+ tracks.push({name: 'Memory per process', series: perProcessSeries});
+ } else {
+ tracks.push.apply(tracks, tr.ui.tracks.buildSystemMemoryChartSeries(
+ this.memoryDumps_[0].model));
+ }
+
+ for (const {name, series} of tracks) {
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = name;
+ track.height = USED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ },
+
+ appendAllocatedMemoryTrack_() {
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ this.memoryDumps_);
+ if (!series) return;
+
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = 'Memory per component';
+ track.height = ALLOCATED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ };
+
+ return {
+ GlobalMemoryDumpTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html
new file mode 100644
index 00000000000..20fa869bbf0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/global_memory_dump_track_test.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/global_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Viewport = tr.ui.TimelineViewport;
+ const GlobalMemoryDumpTrack = tr.ui.tracks.GlobalMemoryDumpTrack;
+ const createTestGlobalMemoryDumps = tr.ui.tracks.createTestGlobalMemoryDumps;
+
+ function instantiateTrack(withVMRegions, withAllocatorDumps,
+ expectedTrackCount) {
+ const dumps = createTestGlobalMemoryDumps(
+ withVMRegions, withAllocatorDumps);
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new GlobalMemoryDumpTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.invalidate();
+
+ track.memoryDumps = dumps;
+ this.addHTMLOutput(div);
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.lengthOf(track.tracks_, expectedTrackCount);
+ }
+
+ test('instantiate_dotsOnly', function() {
+ instantiateTrack.call(this, false, false, 1);
+ });
+
+ test('instantiate_withVMRegions', function() {
+ instantiateTrack.call(this, true, false, 2);
+ });
+
+ test('instantiate_withMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, true, 2);
+ });
+
+ test('instantiate_withBoth', function() {
+ instantiateTrack.call(this, true, true, 3);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html
new file mode 100644
index 00000000000..7ae139672d2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/kernel_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of interaction records.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const InteractionTrack = tr.ui.b.define(
+ 'interaction-track', tr.ui.tracks.MultiRowTrack);
+
+ InteractionTrack.prototype = {
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ this.heading = 'Interactions';
+ this.subRows_ = [];
+ },
+
+ set model(model) {
+ this.setItemsToGroup(model.userModel.expectations, {
+ guid: tr.b.GUID.allocateSimple(),
+ model,
+ getSettingsKey() {
+ return undefined;
+ }
+ });
+ },
+
+ buildSubRows_(slices) {
+ if (this.subRows_.length) {
+ return this.subRows_;
+ }
+ this.subRows_.push(
+ ...tr.ui.tracks.groupAsyncSlicesIntoSubRows(slices, true));
+ return this.subRows_;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ }
+ };
+
+ return {
+ InteractionTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html
new file mode 100644
index 00000000000..ac0d5692c4d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/interaction_track_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/user_model/stub_expectation.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/interaction_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ // UserExpectations should be sorted by start time, not title, so that
+ // AsyncSliceGroupTrack.buildSubRows_ can lay them out in as few tracks as
+ // possible, so that they mesh instead of stacking unnecessarily.
+ test('instantiate', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+ const track = new tr.ui.tracks.InteractionTrack(viewport);
+ track.model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ const thread = process.getOrCreateThread(1);
+ thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 0, duration: 200}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 100, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 0, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 150, duration: 50}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 50, duration: 100}));
+ model.userModel.expectations.push(new tr.model.um.StubExpectation(
+ {parentModel: model, start: 0, duration: 50}));
+ // Model.createImportTracesTask() automatically sorts UEs by start time.
+ });
+ assert.strictEqual(2, track.subRows_.length);
+ assert.strictEqual(2, track.subRows_[0].length);
+ assert.strictEqual(3, track.subRows_[1].length);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html
new file mode 100644
index 00000000000..b10547bc2e9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/kernel_track.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/cpu_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track_base.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const Cpu = tr.model.Cpu;
+ const CpuTrack = tr.ui.tracks.cpu_track;
+ const ProcessTrackBase = tr.ui.tracks.ProcessTrackBase;
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ /**
+ * @constructor
+ */
+ const KernelTrack = tr.ui.b.define('kernel-track', ProcessTrackBase);
+
+ KernelTrack.prototype = {
+ __proto__: ProcessTrackBase.prototype,
+
+ decorate(viewport) {
+ ProcessTrackBase.prototype.decorate.call(this, viewport);
+ },
+
+
+ // Kernel maps to processBase because we derive from ProcessTrackBase.
+ set kernel(kernel) {
+ this.processBase = kernel;
+ },
+
+ get kernel() {
+ return this.processBase;
+ },
+
+ get eventContainer() {
+ return this.kernel;
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 1;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ProcessTrackBase.prototype.addContainersToTrackMap.call(
+ this, containerToTrackMap);
+ containerToTrackMap.addContainer(this.kernel, this);
+ },
+
+ willAppendTracks_() {
+ const cpus = Object.values(this.kernel.cpus);
+ cpus.sort(tr.model.Cpu.compare);
+
+ let didAppendAtLeastOneTrack = false;
+ for (let i = 0; i < cpus.length; ++i) {
+ const cpu = cpus[i];
+ const track = new tr.ui.tracks.CpuTrack(this.viewport);
+ track.detailedMode = this.expanded;
+ track.cpu = cpu;
+ if (!track.hasVisibleContent) continue;
+ Polymer.dom(this).appendChild(track);
+ didAppendAtLeastOneTrack = true;
+ }
+ if (didAppendAtLeastOneTrack) {
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ }
+ };
+
+
+ return {
+ KernelTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html
new file mode 100644
index 00000000000..6a642e52ff9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track.html
@@ -0,0 +1,251 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.letter-dot-track {
+ height: 18px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const EventPresenter = tr.ui.b.EventPresenter;
+ const SelectionState = tr.model.SelectionState;
+
+ /**
+ * A track that displays an array of dots with filled letters inside them.
+ * @constructor
+ * @extends {Track}
+ */
+ const LetterDotTrack = tr.ui.b.define(
+ 'letter-dot-track', tr.ui.tracks.Track);
+
+ LetterDotTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('letter-dot-track');
+ this.items_ = undefined;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get items() {
+ return this.items_;
+ },
+
+ set items(items) {
+ this.items_ = items;
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ get dumpRadiusView() {
+ return 7 * (window.devicePixelRatio || 1);
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ if (this.items_ === undefined) return;
+
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawLetterDots_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawLetterDots_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const bounds = this.getBoundingClientRect();
+ const height = bounds.height * pixelRatio;
+ const halfHeight = height * 0.5;
+ const twoPi = Math.PI * 2;
+
+ // Culling parameters.
+ const dt = this.viewport.currentDisplayTransform;
+ const dumpRadiusView = this.dumpRadiusView;
+ const itemRadiusWorld = dt.xViewVectorToWorld(height);
+
+ // Draw the memory dumps.
+ const items = this.items_;
+ const loI = tr.b.findLowIndexInSortedArray(
+ items,
+ function(item) { return item.start; },
+ viewLWorld);
+
+ const oldFont = ctx.font;
+ ctx.font = '400 ' + Math.floor(9 * pixelRatio) + 'px Arial';
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.textBaseline = 'middle';
+ ctx.textAlign = 'center';
+
+ const drawItems = function(selected) {
+ for (let i = loI; i < items.length; ++i) {
+ const item = items[i];
+ const x = item.start;
+ if (x - itemRadiusWorld > viewRWorld) break;
+
+ if (item.selected !== selected) continue;
+
+ const xView = dt.xWorldToView(x);
+
+ ctx.fillStyle = EventPresenter.getSelectableItemColorAsString(item);
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, dumpRadiusView + 0.5, 0, twoPi);
+ ctx.fill();
+ if (item.selected) {
+ ctx.lineWidth = 3;
+ ctx.strokeStyle = 'rgb(100,100,0)';
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, dumpRadiusView, 0, twoPi);
+ ctx.lineWidth = 1.5;
+ ctx.strokeStyle = 'rgb(255,255,0)';
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.stroke();
+ }
+
+ ctx.fillStyle = 'rgb(255, 255, 255)';
+ ctx.fillText(item.dotLetter, xView, halfHeight);
+ }
+ };
+
+ // Draw unselected items first to make sure they don't occlude selected
+ // items.
+ drawItems(false);
+ drawItems(true);
+
+ ctx.lineWidth = 1;
+ ctx.font = oldFont;
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.items_ === undefined) return;
+
+ this.items_.forEach(function(item) {
+ item.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ if (this.items_ === undefined) return;
+
+ const itemRadiusWorld = viewPixWidthWorld * this.dumpRadiusView;
+ tr.b.iterateOverIntersectingIntervals(
+ this.items_,
+ function(x) { return x.start - itemRadiusWorld; },
+ function(x) { return 2 * itemRadiusWorld; },
+ loWX, hiWX,
+ function(item) {
+ item.addToSelection(selection);
+ }.bind(this));
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {event} The current event item.
+ * @param {Number} offset Number of slices away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (this.items_ === undefined) return;
+
+ const index = this.items_.findIndex(item => item.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < this.items_.length) {
+ this.items_[newIndex].addToSelection(selection);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ if (this.items_ === undefined) return;
+
+ const item = tr.b.findClosestElementInSortedArray(
+ this.items_,
+ function(x) { return x.start; },
+ worldX,
+ worldMaxDist);
+
+ if (!item) return;
+
+ item.addToSelection(selection);
+ }
+ };
+
+ /**
+ * A filled dot with a letter inside it.
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function LetterDot(modelItem, dotLetter, colorId, start) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.dotLetter = dotLetter;
+ this.colorId = colorId;
+ this.start = start;
+ }
+
+ LetterDot.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype
+ };
+
+ return {
+ LetterDotTrack,
+ LetterDot,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html
new file mode 100644
index 00000000000..b37034afab2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/letter_dot_track_test.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const LetterDotTrack = tr.ui.tracks.LetterDotTrack;
+ const LetterDot = tr.ui.tracks.LetterDot;
+ const SelectionState = tr.model.SelectionState;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createItems = function() {
+ const items = [
+ new LetterDot({selectionState: SelectionState.SELECTED}, 'a', 7, 5),
+ new LetterDot({selectionState: SelectionState.SELECTED}, 'b', 2, 20),
+ new LetterDot({selectionState: SelectionState.NONE}, 'c', 4, 35),
+ new LetterDot({selectionState: SelectionState.NONE}, 'd', 4, 50)
+ ];
+ return items;
+ };
+
+ test('instantiate', function() {
+ const items = createItems();
+
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = LetterDotTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.items = items;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 60, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('selectionHitTesting', function() {
+ const items = createItems();
+
+ const track = new LetterDotTrack(new Viewport());
+ track.items = items;
+
+ // Fake a view pixel size.
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ const viewPixWidthWorld = 0.1 / devicePixelRatio;
+
+ // Hit outside range
+ let selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 3, 4, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 0);
+
+ // Hit the first item, via pixel-nearness.
+ selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 19.98, 19.99, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.strictEqual(selection[0], items[1].modelItem);
+
+ // Hit the instance, between the 1st and 2nd snapshots
+ selection = [];
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 30, 50, viewPixWidthWorld, selection);
+ assert.strictEqual(selection.length, 2);
+ assert.strictEqual(selection[0], items[2].modelItem);
+ assert.strictEqual(selection[1], items[3].modelItem);
+ });
+
+ test('addEventNearToProvidedEventToSelection', function() {
+ const items = createItems();
+
+ const track = new LetterDotTrack(new Viewport());
+ track.items = items;
+
+ // Right from the middle of items.
+ const selection1 = [];
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ items[2].modelItem, 1, selection1));
+ assert.strictEqual(selection1.length, 1);
+ assert.strictEqual(selection1[0], items[3].modelItem);
+
+ // Left from the middle of items.
+ const selection2 = [];
+ assert.isTrue(track.addEventNearToProvidedEventToSelection(
+ items[2].modelItem, -1, selection2));
+ assert.strictEqual(selection2.length, 1);
+ assert.strictEqual(selection2[0], items[1].modelItem);
+
+ // Right from the right edge of items.
+ const selection3 = [];
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ items[3].modelItem, 1, selection3));
+ assert.strictEqual(selection3.length, 0);
+
+ // Left from the left edge of items.
+ const selection4 = [];
+ assert.isFalse(track.addEventNearToProvidedEventToSelection(
+ items[0].modelItem, -1, selection4));
+ assert.strictEqual(selection4.length, 0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html
new file mode 100644
index 00000000000..611bd8664f8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_test_utils.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/global_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/process_memory_dump.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/model/vm_region.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for memory dump track tests.
+ */
+tr.exportTo('tr.ui.tracks', function() {
+ const ProcessMemoryDump = tr.model.ProcessMemoryDump;
+ const GlobalMemoryDump = tr.model.GlobalMemoryDump;
+ const VMRegion = tr.model.VMRegion;
+ const VMRegionClassificationNode = tr.model.VMRegionClassificationNode;
+ const SelectionState = tr.model.SelectionState;
+ const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump;
+ const addProcessMemoryDump =
+ tr.model.MemoryDumpTestUtils.addProcessMemoryDump;
+ const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump;
+ const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink;
+ const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ function createVMRegions(pssValues) {
+ return VMRegionClassificationNode.fromRegions(
+ pssValues.map(function(pssValue, i) {
+ return VMRegion.fromDict({
+ startAddress: 1000 * i,
+ sizeInBytes: 1000,
+ protectionFlags: VMRegion.PROTECTION_FLAG_READ,
+ mappedFile: '[stack' + i + ']',
+ byteStats: {
+ privateDirtyResident: pssValue / 3,
+ swapped: pssValue * 3,
+ proportionalResident: pssValue
+ }
+ });
+ }));
+ }
+
+ function createAllocatorDumps(memoryDump, dumpData) {
+ // Create the individual allocator dumps.
+ const allocatorDumps = {};
+ for (const [allocatorName, data] of Object.entries(dumpData)) {
+ const size = data.size;
+ assert.typeOf(size, 'number'); // Sanity check.
+ allocatorDumps[allocatorName] = newAllocatorDump(
+ memoryDump, allocatorName, {numerics: {size}});
+ }
+
+ // Add ownership links between them.
+ for (const [allocatorName, data] of Object.entries(dumpData)) {
+ const owns = data.owns;
+ if (owns === undefined) continue;
+
+ const ownerDump = allocatorDumps[allocatorName];
+ assert.isDefined(ownerDump); // Sanity check.
+ const ownedDump = allocatorDumps[owns];
+ assert.isDefined(ownedDump); // Sanity check.
+
+ addOwnershipLink(ownerDump, ownedDump);
+ }
+
+ return Object.values(allocatorDumps);
+ }
+
+ function addProcessMemoryDumpWithFields(globalMemoryDump, process, start,
+ opt_pssValues, opt_dumpData) {
+ const pmd = addProcessMemoryDump(globalMemoryDump, process, {ts: start});
+ if (opt_pssValues !== undefined) {
+ pmd.vmRegions = createVMRegions(opt_pssValues);
+ }
+ if (opt_dumpData !== undefined) {
+ pmd.memoryAllocatorDumps = createAllocatorDumps(pmd, opt_dumpData);
+ }
+ }
+
+ function createModelWithDumps(withVMRegions, withAllocatorDumps) {
+ const maybePssValues = function(pssValues) {
+ return withVMRegions ? pssValues : undefined;
+ };
+ const maybeDumpData = function(dumpData) {
+ return withAllocatorDumps ? dumpData : undefined;
+ };
+ return tr.c.TestUtils.newModel(function(model) {
+ // Construct a model with three processes.
+ const pa = model.getOrCreateProcess(3);
+ const pb = model.getOrCreateProcess(6);
+ const pc = model.getOrCreateProcess(9);
+
+ const gmd1 = addGlobalMemoryDump(model, {ts: 0, levelOfDetail: LIGHT});
+ addProcessMemoryDumpWithFields(gmd1, pa, 0, maybePssValues([111]));
+ addProcessMemoryDumpWithFields(gmd1, pb, 0.2, undefined,
+ maybeDumpData({oilpan: {size: 1024}}));
+
+ const gmd2 = addGlobalMemoryDump(model, {ts: 5, levelOfDetail: DETAILED});
+ addProcessMemoryDumpWithFields(gmd2, pa, 0);
+ addProcessMemoryDumpWithFields(gmd2, pb, 4.99, maybePssValues([100, 50]),
+ maybeDumpData({v8: {size: 512}}));
+ addProcessMemoryDumpWithFields(gmd2, pc, 5.12, undefined,
+ maybeDumpData({oilpan: {size: 128, owns: 'v8'},
+ v8: {size: 384, owns: 'tracing'}, tracing: {size: 65920}}));
+
+ const gmd3 = addGlobalMemoryDump(
+ model, {ts: 15, levelOfDetail: DETAILED});
+ addProcessMemoryDumpWithFields(gmd3, pa, 15.5, maybePssValues([]),
+ maybeDumpData({v8: {size: 768}}));
+ addProcessMemoryDumpWithFields(gmd3, pc, 14.5,
+ maybePssValues([70, 70, 70]), maybeDumpData({oilpan: {size: 512}}));
+
+ const gmd4 = addGlobalMemoryDump(model, {ts: 18, levelOfDetail: LIGHT});
+
+ const gmd5 = addGlobalMemoryDump(model,
+ {ts: 20, levelOfDetail: BACKGROUND});
+ addProcessMemoryDumpWithFields(gmd5, pa, 0, maybePssValues([105]));
+ addProcessMemoryDumpWithFields(gmd5, pb, 0.2, undefined,
+ maybeDumpData({oilpan: {size: 100}}));
+ });
+ }
+
+ function createTestGlobalMemoryDumps(withVMRegions, withAllocatorDumps) {
+ const model = createModelWithDumps(withVMRegions, withAllocatorDumps);
+ const dumps = model.globalMemoryDumps;
+ dumps[1].selectionState = SelectionState.HIGHLIGHTED;
+ dumps[2].selectionState = SelectionState.SELECTED;
+ return dumps;
+ }
+
+ function createTestProcessMemoryDumps(withVMRegions, withAllocatorDumps) {
+ const model = createModelWithDumps(withVMRegions, withAllocatorDumps);
+ const dumps = model.getProcess(9).memoryDumps;
+ dumps[0].selectionState = SelectionState.SELECTED;
+ dumps[1].selectionState = SelectionState.HIGHLIGHTED;
+ return dumps;
+ }
+
+ return {
+ createTestGlobalMemoryDumps,
+ createTestProcessMemoryDumps,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html
new file mode 100644
index 00000000000..a483d545880
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util.html
@@ -0,0 +1,253 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/container_memory_dump.html">
+<link rel="import" href="/tracing/model/memory_allocator_dump.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ const DISPLAYED_SIZE_NUMERIC_NAME =
+ tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME;
+ const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
+ const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
+ const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
+
+ const SYSTEM_MEMORY_CHART_RENDERING_CONFIG = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: ColorScheme.getColorIdForGeneralPurposeString('systemMemory'),
+ backgroundOpacity: 0.8
+ };
+ const SYSTEM_MEMORY_SERIES_NAMES = ['Used (KB)', 'Swapped (KB)'];
+
+ /** Extract PSS values of processes in a global memory dump. */
+ function extractGlobalMemoryDumpUsedSizes(globalMemoryDump, addSize) {
+ for (const [pid, pmd] of
+ Object.entries(globalMemoryDump.processMemoryDumps)) {
+ const mostRecentVmRegions = pmd.mostRecentVmRegions;
+ if (mostRecentVmRegions === undefined) continue;
+ addSize(pid, mostRecentVmRegions.byteStats.proportionalResident || 0,
+ pmd.process.userFriendlyName);
+ }
+ }
+
+ /** Extract sizes of root allocators in a process memory dump. */
+ function extractProcessMemoryDumpAllocatorSizes(processMemoryDump, addSize) {
+ const allocatorDumps = processMemoryDump.memoryAllocatorDumps;
+ if (allocatorDumps === undefined) return;
+
+ allocatorDumps.forEach(function(allocatorDump) {
+ // Don't show tracing overhead in the charts.
+ // TODO(petrcermak): Find a less hacky way to do this.
+ if (allocatorDump.fullName === 'tracing') return;
+
+ const allocatorSize = allocatorDump.numerics[DISPLAYED_SIZE_NUMERIC_NAME];
+ if (allocatorSize === undefined) return;
+
+ const allocatorSizeValue = allocatorSize.value;
+ if (allocatorSizeValue === undefined) return;
+
+ addSize(allocatorDump.fullName, allocatorSizeValue);
+ });
+ }
+
+ /** Extract sizes of root allocators in a global memory dump. */
+ function extractGlobalMemoryDumpAllocatorSizes(globalMemoryDump, addSize) {
+ for (const pmd of Object.values(globalMemoryDump.processMemoryDumps)) {
+ extractProcessMemoryDumpAllocatorSizes(pmd, addSize);
+ }
+ }
+
+ /**
+ * A generic function which converts a list of memory dumps to a list of
+ * chart series.
+ *
+ * @param {!Array<!tr.model.ContainerMemoryDump>} memoryDumps List of
+ * container memory dumps.
+ * @param {!function(
+ * !tr.model.ContainerMemoryDump,
+ * !function(string, number, string=))} dumpSizeExtractor Callback for
+ * extracting sizes from a container memory dump.
+ * @return {(!Array<!tr.ui.tracks.ChartSeries>|undefined)} List of chart
+ * series (or undefined if no size is extracted from any container memory
+ * dump).
+ */
+ function buildMemoryChartSeries(memoryDumps, dumpSizeExtractor) {
+ const dumpCount = memoryDumps.length;
+ const idToTimestampToPoint = {};
+ const idToName = {};
+
+ // Extract the sizes of all components from each memory dump.
+ memoryDumps.forEach(function(dump, index) {
+ dumpSizeExtractor(dump, function addSize(id, size, opt_name) {
+ let timestampToPoint = idToTimestampToPoint[id];
+ if (timestampToPoint === undefined) {
+ idToTimestampToPoint[id] = timestampToPoint = new Array(dumpCount);
+ for (let i = 0; i < dumpCount; i++) {
+ const modelItem = memoryDumps[i];
+ timestampToPoint[i] = new tr.ui.tracks.ChartPoint(
+ modelItem, modelItem.start, 0);
+ }
+ }
+ timestampToPoint[index].y += size;
+ if (opt_name !== undefined) idToName[id] = opt_name;
+ });
+ });
+
+ // Do not generate any chart series if no sizes were extracted.
+ const ids = Object.keys(idToTimestampToPoint);
+ if (ids.length === 0) return undefined;
+
+ ids.sort();
+ for (let i = 0; i < dumpCount; i++) {
+ let baseSize = 0;
+ // Traverse |ids| in reverse (alphabetical) order so that the first id is
+ // at the top of the chart.
+ for (let j = ids.length - 1; j >= 0; j--) {
+ const point = idToTimestampToPoint[ids[j]][i];
+ point.yBase = baseSize;
+ point.y += baseSize;
+ baseSize = point.y;
+ }
+ }
+
+ // Create one common axis for all memory chart series.
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0);
+
+ // Build a chart series for each id.
+ const series = ids.map(function(id) {
+ const colorId = ColorScheme.getColorIdForGeneralPurposeString(
+ idToName[id] || id);
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId,
+ backgroundOpacity: 0.8
+ };
+ return new tr.ui.tracks.ChartSeries(idToTimestampToPoint[id],
+ seriesYAxis, renderingConfig);
+ });
+
+ // Ensure that the series at the top of the chart are drawn last.
+ series.reverse();
+
+ return series;
+ }
+
+ /**
+ * Transform a list of memory dumps to a list of letter dots (with letter 'M'
+ * inside).
+ */
+ function buildMemoryLetterDots(memoryDumps) {
+ const backgroundMemoryColorId =
+ ColorScheme.getColorIdForReservedName('background_memory_dump');
+ const lightMemoryColorId =
+ ColorScheme.getColorIdForReservedName('light_memory_dump');
+ const detailedMemoryColorId =
+ ColorScheme.getColorIdForReservedName('detailed_memory_dump');
+ return memoryDumps.map(function(memoryDump) {
+ let memoryColorId;
+ switch (memoryDump.levelOfDetail) {
+ case BACKGROUND:
+ memoryColorId = backgroundMemoryColorId;
+ break;
+ case DETAILED:
+ memoryColorId = detailedMemoryColorId;
+ break;
+ case LIGHT:
+ default:
+ memoryColorId = lightMemoryColorId;
+ }
+ return new tr.ui.tracks.LetterDot(
+ memoryDump, 'M', memoryColorId, memoryDump.start);
+ });
+ }
+
+ /**
+ * Convert a list of global memory dumps to a list of chart series (one per
+ * process). Each series represents the evolution of the memory used by the
+ * process over time.
+ */
+ function buildGlobalUsedMemoryChartSeries(globalMemoryDumps) {
+ return buildMemoryChartSeries(globalMemoryDumps,
+ extractGlobalMemoryDumpUsedSizes);
+ }
+
+ /**
+ * Convert a list of process memory dumps to a list of chart series (one per
+ * root allocator). Each series represents the evolution of the size of a the
+ * corresponding root allocator (e.g. 'v8') over time.
+ */
+ function buildProcessAllocatedMemoryChartSeries(processMemoryDumps) {
+ return buildMemoryChartSeries(processMemoryDumps,
+ extractProcessMemoryDumpAllocatorSizes);
+ }
+
+ /**
+ * Convert a list of global memory dumps to a list of chart series (one per
+ * root allocator). Each series represents the evolution of the size of a the
+ * corresponding root allocator (e.g. 'v8') over time.
+ */
+ function buildGlobalAllocatedMemoryChartSeries(globalMemoryDumps) {
+ return buildMemoryChartSeries(globalMemoryDumps,
+ extractGlobalMemoryDumpAllocatorSizes);
+ }
+
+ /**
+ * Converts system memory counters in the model to a list of
+ * {'name': trackName, 'series': ChartSeries}.
+ */
+ function buildSystemMemoryChartSeries(model) {
+ if (model.kernel.counters === undefined) return;
+ const memoryCounter = model.kernel.counters['global.SystemMemory'];
+ if (memoryCounter === undefined) return;
+
+ const tracks = [];
+ for (const name of SYSTEM_MEMORY_SERIES_NAMES) {
+ const series = memoryCounter.series.find(series => series.name === name);
+ if (series === undefined || series.samples.length === 0) return;
+
+ const chartPoints = [];
+ const valueRange = new tr.b.math.Range();
+ for (const sample of series.samples) {
+ chartPoints.push(new tr.ui.tracks.ChartPoint(
+ sample, sample.timestamp, sample.value, 0));
+ valueRange.addValue(sample.value);
+ }
+ // Stretch min to max range over the top half of a chart for readability.
+ const baseLine = Math.max(0, valueRange.min - valueRange.range);
+ const axisY = new tr.ui.tracks.ChartSeriesYAxis(baseLine, valueRange.max);
+ const chartSeries =
+ [new tr.ui.tracks.ChartSeries(chartPoints, axisY,
+ SYSTEM_MEMORY_CHART_RENDERING_CONFIG)];
+ tracks.push({
+ name: 'System Memory ' + name,
+ series: chartSeries
+ });
+ }
+ return tracks;
+ }
+
+ return {
+ buildMemoryLetterDots,
+ buildGlobalUsedMemoryChartSeries,
+ buildProcessAllocatedMemoryChartSeries,
+ buildGlobalAllocatedMemoryChartSeries,
+ buildSystemMemoryChartSeries,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html
new file mode 100644
index 00000000000..f4f9451b4b9
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_dump_track_util_test.html
@@ -0,0 +1,270 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SelectionState = tr.model.SelectionState;
+ const createTestGlobalMemoryDumps = tr.ui.tracks.createTestGlobalMemoryDumps;
+ const createTestProcessMemoryDumps =
+ tr.ui.tracks.createTestProcessMemoryDumps;
+
+ test('buildMemoryLetterDots_withoutVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const items = tr.ui.tracks.buildMemoryLetterDots(dumps);
+
+ assert.lengthOf(items, 5);
+ assert.strictEqual(items[0].start, 0);
+ assert.strictEqual(items[1].start, 5);
+ assert.strictEqual(items[2].start, 15);
+ assert.strictEqual(items[3].start, 18);
+ assert.strictEqual(items[4].start, 20);
+
+ // Check model mapping.
+ assert.strictEqual(items[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(items[2].selected);
+ assert.strictEqual(items[3].modelItem, dumps[3]);
+ });
+
+ test('buildMemoryLetterDots_withVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const items = tr.ui.tracks.buildMemoryLetterDots(dumps);
+
+ assert.lengthOf(items, 5);
+ assert.strictEqual(items[0].start, 0);
+ assert.strictEqual(items[1].start, 5);
+ assert.strictEqual(items[2].start, 15);
+ assert.strictEqual(items[3].start, 18);
+ assert.strictEqual(items[4].start, 20);
+
+ // Check model mapping.
+ assert.strictEqual(items[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(items[2].selected);
+ assert.strictEqual(items[3].modelItem, dumps[3]);
+ });
+
+ test('buildGlobalUsedMemoryChartSeries_withoutVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildGlobalUsedMemoryChartSeries(dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildGlobalUsedMemoryChartSeries_withVMRegions', function() {
+ const dumps = createTestGlobalMemoryDumps(true, false);
+ const series = tr.ui.tracks.buildGlobalUsedMemoryChartSeries(dumps);
+
+ assert.lengthOf(series, 3);
+
+ const sa = series[2];
+ const sb = series[1];
+ const sc = series[0];
+
+ assert.lengthOf(sa.points, 5);
+ assert.lengthOf(sb.points, 5);
+ assert.lengthOf(sc.points, 5);
+
+ // Process A: VM regions defined -> sum their PSS values (111).
+ // Process B: VM regions undefined and no previous value -> assume zero.
+ // Process C: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[0].x, 0);
+ assert.strictEqual(sb.points[0].x, 0);
+ assert.strictEqual(sc.points[0].x, 0);
+ assert.strictEqual(sa.points[0].y, 111);
+ assert.strictEqual(sb.points[0].y, 0);
+ assert.strictEqual(sc.points[0].y, 0);
+ assert.strictEqual(sa.points[0].yBase, 0);
+ assert.strictEqual(sb.points[0].yBase, 0);
+ assert.strictEqual(sc.points[0].yBase, 0);
+
+ // Process A: VM regions undefined -> assume previous value (111).
+ // Process B: VM regions defined -> sum their PSS values (100 + 50).
+ // Process C: VM regions undefined -> assume previous value (0).
+ assert.strictEqual(sa.points[1].x, 5);
+ assert.strictEqual(sb.points[1].x, 5);
+ assert.strictEqual(sc.points[1].x, 5);
+ assert.strictEqual(sa.points[1].y, 150 + 111);
+ assert.strictEqual(sb.points[1].y, 150);
+ assert.strictEqual(sc.points[1].y, 0);
+ assert.strictEqual(sa.points[1].yBase, 150);
+ assert.strictEqual(sb.points[1].yBase, 0);
+ assert.strictEqual(sc.points[1].yBase, 0);
+
+ // Process A: VM regions defined -> sum their PSS values (0).
+ // Process B: Memory dump not present -> assume process not alive (0).
+ // Process C: VM regions defined -> sum their PSS values (70 + 70 + 70).
+ assert.strictEqual(sa.points[2].x, 15);
+ assert.strictEqual(sb.points[2].x, 15);
+ assert.strictEqual(sc.points[2].x, 15);
+ assert.strictEqual(sa.points[2].y, 210);
+ assert.strictEqual(sb.points[2].y, 210);
+ assert.strictEqual(sc.points[2].y, 210);
+ assert.strictEqual(sa.points[2].yBase, 210);
+ assert.strictEqual(sb.points[2].yBase, 210);
+ assert.strictEqual(sc.points[2].yBase, 0);
+
+ // All processes: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[3].x, 18);
+ assert.strictEqual(sb.points[3].x, 18);
+ assert.strictEqual(sc.points[3].x, 18);
+ assert.strictEqual(sa.points[3].y, 0);
+ assert.strictEqual(sb.points[3].y, 0);
+ assert.strictEqual(sc.points[3].y, 0);
+ assert.strictEqual(sa.points[3].yBase, 0);
+ assert.strictEqual(sb.points[3].yBase, 0);
+ assert.strictEqual(sc.points[3].yBase, 0);
+
+ // Process A: VM regions defined -> sum their PSS values (105).
+ // Process B: VM regions undefined and no previous value -> assume zero.
+ // Process C: Memory dump not present -> assume process not alive (0).
+ assert.strictEqual(sa.points[4].x, 20);
+ assert.strictEqual(sb.points[4].x, 20);
+ assert.strictEqual(sc.points[4].x, 20);
+ assert.strictEqual(sa.points[4].y, 105);
+ assert.strictEqual(sb.points[4].y, 0);
+ assert.strictEqual(sc.points[4].y, 0);
+ assert.strictEqual(sa.points[4].yBase, 0);
+ assert.strictEqual(sb.points[4].yBase, 0);
+ assert.strictEqual(sc.points[4].yBase, 0);
+
+ // Check model mapping.
+ assert.strictEqual(sa.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sb.points[2].selected);
+ assert.strictEqual(sc.points[3].modelItem, dumps[3]);
+ });
+
+ test('buildGlobalAllocatedMemoryChartSeries_withoutMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestGlobalMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildGlobalAllocatedMemoryChartSeries_withMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestGlobalMemoryDumps(false, true);
+ const series = tr.ui.tracks.buildGlobalAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.lengthOf(series, 2);
+
+ const so = series[1];
+ const sv = series[0];
+
+ assert.lengthOf(so.points, 5);
+ assert.lengthOf(sv.points, 5);
+
+ // Oilpan: Only process B dumps allocated objects size (1024).
+ // V8: No process dumps allocated objects size (0).
+ assert.strictEqual(so.points[0].x, 0);
+ assert.strictEqual(sv.points[0].x, 0);
+ assert.strictEqual(so.points[0].y, 1024);
+ assert.strictEqual(sv.points[0].y, 0);
+ assert.strictEqual(so.points[0].yBase, 0);
+ assert.strictEqual(sv.points[0].yBase, 0);
+
+ // Oilpan: Process B did not provide a value and process C dumps (128).
+ // V8: Processes B and C dump (512 + 256).
+ assert.strictEqual(so.points[1].x, 5);
+ assert.strictEqual(sv.points[1].x, 5);
+ assert.strictEqual(so.points[1].y, 768 + 128);
+ assert.strictEqual(sv.points[1].y, 768);
+ assert.strictEqual(so.points[1].yBase, 768);
+ assert.strictEqual(sv.points[1].yBase, 0);
+
+ // Oilpan: Process B assumed not alive and process C dumps (512)
+ // V8: Process A dumps now, process B assumed not alive, process C did
+ // not provide a value (768).
+ assert.strictEqual(so.points[2].x, 15);
+ assert.strictEqual(sv.points[2].x, 15);
+ assert.strictEqual(so.points[2].y, 768 + 512);
+ assert.strictEqual(sv.points[2].y, 768);
+ assert.strictEqual(so.points[2].yBase, 768);
+ assert.strictEqual(sv.points[2].yBase, 0);
+
+ // All processes: Memory dump not present -> assume process not alive
+ // (0).
+ assert.strictEqual(so.points[3].x, 18);
+ assert.strictEqual(sv.points[3].x, 18);
+ assert.strictEqual(so.points[3].y, 0);
+ assert.strictEqual(sv.points[3].y, 0);
+ assert.strictEqual(so.points[3].yBase, 0);
+ assert.strictEqual(sv.points[3].yBase, 0);
+
+ // Oilpan: Only process B dumps allocated objects size (100).
+ // V8: No process dumps allocated objects size (0).
+ assert.strictEqual(so.points[4].x, 20);
+ assert.strictEqual(sv.points[4].x, 20);
+ assert.strictEqual(so.points[4].y, 100);
+ assert.strictEqual(sv.points[4].y, 0);
+
+ // Check model mapping.
+ assert.strictEqual(
+ so.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sv.points[2].selected);
+ assert.strictEqual(so.points[3].modelItem, dumps[3]);
+ });
+
+ test('buildProcessAllocatedMemoryChartSeries_withoutMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestProcessMemoryDumps(false, false);
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ dumps);
+
+ assert.isUndefined(series);
+ });
+
+ test('buildProcessAllocatedMemoryChartSeries_withMemoryAllocatorDumps',
+ function() {
+ const dumps = createTestProcessMemoryDumps(false, true);
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ dumps);
+
+ // There should be only 2 series (because 'tracing' is not shown in the
+ // charts).
+ assert.lengthOf(series, 2);
+
+ const so = series[1];
+ const sv = series[0];
+
+ assert.lengthOf(so.points, 2);
+ assert.lengthOf(sv.points, 2);
+
+ // Oilpan: Process dumps (128).
+ // V8: Process dumps (256).
+ assert.strictEqual(so.points[0].x, 5.12);
+ assert.strictEqual(sv.points[0].x, 5.12);
+ assert.strictEqual(so.points[0].y, 256 + 128);
+ assert.strictEqual(sv.points[0].y, 256);
+ assert.strictEqual(so.points[0].yBase, 256);
+ assert.strictEqual(sv.points[0].yBase, 0);
+
+ // Oilpan: Process dumps (512).
+ // V8: Process did not provide a value (0).
+ assert.strictEqual(so.points[1].x, 14.5);
+ assert.strictEqual(sv.points[1].x, 14.5);
+ assert.strictEqual(so.points[1].y, 512);
+ assert.strictEqual(sv.points[1].y, 0);
+ assert.strictEqual(so.points[1].yBase, 0);
+ assert.strictEqual(sv.points[1].yBase, 0);
+
+ // Check model mapping.
+ assert.strictEqual(
+ so.points[1].selectionState, SelectionState.HIGHLIGHTED);
+ assert.isTrue(sv.points[0].selected);
+ assert.strictEqual(so.points[1].modelItem, dumps[1]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html
new file mode 100644
index 00000000000..bf59f11349e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright 2017 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const LetterDotTrack = tr.ui.tracks.LetterDotTrack;
+
+ /**
+ * A track that displays global memory events.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.LetterDotTrack}
+ */
+ const MemoryTrack = tr.ui.b.define('memory-track', LetterDotTrack);
+
+ MemoryTrack.prototype = {
+ __proto__: LetterDotTrack.prototype,
+
+ decorate(viewport) {
+ LetterDotTrack.prototype.decorate.call(this, viewport);
+ this.classList.add('memory-track');
+ this.heading = 'Memory Events';
+ this.lowMemoryEvents_ = undefined;
+ },
+
+ initialize(model) {
+ if (model !== undefined) {
+ this.lowMemoryEvents_ = model.device.lowMemoryEvents;
+ } else {
+ this.lowMemoryEvents_ = undefined;
+ }
+
+ if (this.hasVisibleContent) {
+ this.items = this.buildMemoryLetterDots_(this.lowMemoryEvents_);
+ }
+ },
+
+ get hasVisibleContent() {
+ return !!this.lowMemoryEvents_ && this.lowMemoryEvents_.length !== 0;
+ },
+
+ buildMemoryLetterDots_(memoryEvents) {
+ return memoryEvents.map(
+ memoryEvent => new tr.ui.tracks.LetterDot(
+ memoryEvent,
+ 'K',
+ ColorScheme.getColorIdForReservedName('background_memory_dump'),
+ memoryEvent.start
+ )
+ );
+ },
+ };
+
+ return {
+ MemoryTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html
new file mode 100644
index 00000000000..60f1c8ccd2c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/memory_track_test.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2017 The Chromium Authors. All rights reserved.
+ Use of this source code is governed by a BSD-style license that can be
+ found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/extras/memory/lowmemory_auditor.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/thread_slice.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/memory_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Model = tr.Model;
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ // Input : slices is an array-of-array-of slices. Each top level array
+ // represents a process. So, each slice in one of the top level array
+ // will be placed in the same process.
+ function buildModel(slices) {
+ const model = tr.c.TestUtils.newModel(function(model) {
+ const process = model.getOrCreateProcess(1);
+ for (let i = 0; i < slices.length; i++) {
+ const thread = process.getOrCreateThread(i);
+ slices[i].forEach(s => thread.sliceGroup.pushSlice(s));
+ }
+ });
+
+ const auditor = new tr.e.audits.LowMemoryAuditor(model);
+ auditor.runAnnotate();
+ return model;
+ }
+
+ function createMemoryTrack(model, interval) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ div.appendChild(drawingContainer);
+ const track = new tr.ui.tracks.MemoryTrack(drawingContainer.viewport);
+ if (model !== undefined) {
+ setDisplayTransformFromBounds(viewport, model.bounds);
+ }
+ track.initialize(model, interval);
+ drawingContainer.appendChild(track);
+ this.addHTMLOutput(drawingContainer);
+ return track;
+ }
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ function setDisplayTransformFromBounds(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ }
+
+ test('instantiate', function() {
+ const sliceA = new tr.model.ThreadSlice('lowmemory', title, 0, 5.5111, {});
+ const sliceB = new tr.model.ThreadSlice('lowmemory', title, 0, 11.2384, {});
+
+ const model = buildModel([[sliceA, sliceB]]);
+ createMemoryTrack.call(this, model);
+ });
+
+ test('hasVisibleContent_trueWithThreadSlicePresent', function() {
+ const sliceA = new tr.model.ThreadSlice('lowmemory', title, 0, 5.5111, {});
+ const sliceB = new tr.model.ThreadSlice('lowmemory', title, 0, 11.2384, {});
+ const model = buildModel([[sliceA, sliceB]]);
+ const track = createMemoryTrack.call(this, model);
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedProcessModel', function() {
+ const track = createMemoryTrack.call(this, undefined);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithNoThreadSlice', function() {
+ const sliceA = new tr.model.ThreadSlice('', title, 0, 7.6211, {});
+ const model = buildModel([[sliceA]]);
+ const track = createMemoryTrack.call(this, model);
+
+ assert.isFalse(track.hasVisibleContent);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html
new file mode 100644
index 00000000000..45404899b7c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track.html
@@ -0,0 +1,534 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/alert_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/cpu_usage_track.html">
+<link rel="import" href="/tracing/ui/tracks/device_track.html">
+<link rel="import" href="/tracing/ui/tracks/global_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/interaction_track.html">
+<link rel="import" href="/tracing/ui/tracks/kernel_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track.html">
+
+<style>
+.model-track {
+ flex-grow: 1;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const SelectionState = tr.model.SelectionState;
+ const ColorScheme = tr.b.ColorScheme;
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ /**
+ * Visualizes a Model by building ProcessTracks and CpuTracks.
+ * @constructor
+ */
+ const ModelTrack = tr.ui.b.define('model-track', tr.ui.tracks.ContainerTrack);
+
+ ModelTrack.VSYNC_HIGHLIGHT_ALPHA = 0.1;
+ ModelTrack.VSYNC_DENSITY_TRANSPARENT = 0.20;
+ ModelTrack.VSYNC_DENSITY_OPAQUE = 0.10;
+ ModelTrack.VSYNC_DENSITY_RANGE =
+ ModelTrack.VSYNC_DENSITY_TRANSPARENT - ModelTrack.VSYNC_DENSITY_OPAQUE;
+
+ /**
+ * Generate a zebra striping from a list of times.
+ *
+ * @param {!Array.<number>} times A sorted array of timestamps.
+ * @param {number} minTime the lower bound of time to start striping at.
+ * @param {number} maxTime the upper bound of time to stop striping at.
+ * of |times|.
+ *
+ * @returns {!Array.<tr.b.math.Range>} An array of ranges where each element
+ * represents the time range that a stripe covers. Each range is a subset
+ * of the interval [minTime, maxTime].
+ */
+ ModelTrack.generateStripes_ = function(times, minTime, maxTime) {
+ if (times.length === 0) return [];
+
+ // Find the lowest and highest index within the viewport.
+ const lowIndex = tr.b.findLowIndexInSortedArray(
+ times, (x => x), minTime);
+ let highIndex = lowIndex - 1;
+ while (times[highIndex + 1] <= maxTime) {
+ highIndex++;
+ }
+
+ const stripes = [];
+ // Must start at an even index and end at an odd index.
+ for (let i = lowIndex - (lowIndex % 2); i <= highIndex; i += 2) {
+ const left = i < lowIndex ? minTime : times[i];
+ const right = i + 1 > highIndex ? maxTime : times[i + 1];
+ stripes.push(tr.b.math.Range.fromExplicitRange(left, right));
+ }
+
+ return stripes;
+ };
+
+
+ ModelTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('model-track');
+
+ this.upperMode_ = false;
+ this.annotationViews_ = [];
+ this.vSyncTimes_ = [];
+ },
+
+ get processViews() {
+ return Polymer.dom(this).querySelectorAll('.process-track-base');
+ },
+
+ // upperMode is true if the track is being used on the ruler.
+ get upperMode() {
+ return this.upperMode_;
+ },
+
+ set upperMode(upperMode) {
+ this.upperMode_ = upperMode;
+ this.updateContents_();
+ },
+
+ detach() {
+ tr.ui.tracks.ContainerTrack.prototype.detach.call(this);
+ },
+
+ get model() {
+ return this.model_;
+ },
+
+ set model(model) {
+ this.model_ = model;
+ this.updateContents_();
+
+ this.model_.addEventListener('annotationChange',
+ this.updateAnnotations_.bind(this));
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 0;
+ },
+
+ updateContents_() {
+ Polymer.dom(this).textContent = '';
+ if (!this.model_) return;
+
+ if (this.upperMode_) {
+ this.updateContentsForUpperMode_();
+ } else {
+ this.updateContentsForLowerMode_();
+ }
+ },
+
+ updateContentsForUpperMode_() {
+ },
+
+ updateContentsForLowerMode_() {
+ if (this.model_.userModel.expectations.length > 1) {
+ const mrt = new tr.ui.tracks.InteractionTrack(this.viewport_);
+ mrt.model = this.model_;
+ Polymer.dom(this).appendChild(mrt);
+ }
+
+ if (this.model_.alerts.length) {
+ const at = new tr.ui.tracks.AlertTrack(this.viewport_);
+ at.alerts = this.model_.alerts;
+ Polymer.dom(this).appendChild(at);
+ }
+
+ if (this.model_.globalMemoryDumps.length) {
+ const gmdt = new tr.ui.tracks.GlobalMemoryDumpTrack(this.viewport_);
+ gmdt.memoryDumps = this.model_.globalMemoryDumps;
+ Polymer.dom(this).appendChild(gmdt);
+ }
+
+ this.appendDeviceTrack_();
+ this.appendCpuUsageTrack_();
+ this.appendMemoryTrack_();
+ this.appendKernelTrack_();
+
+ // Get a sorted list of processes.
+ const processes = this.model_.getAllProcesses();
+ processes.sort(tr.model.Process.compare);
+
+ for (let i = 0; i < processes.length; ++i) {
+ const process = processes[i];
+
+ const track = new tr.ui.tracks.ProcessTrack(this.viewport);
+ track.process = process;
+ if (!track.hasVisibleContent) continue;
+
+ Polymer.dom(this).appendChild(track);
+ }
+ this.viewport_.rebuildEventToTrackMap();
+ this.viewport_.rebuildContainerToTrackMap();
+ this.vSyncTimes_ = this.model_.device.vSyncTimestamps;
+
+ this.updateAnnotations_();
+ },
+
+ getContentBounds() { return this.model.bounds; },
+
+ addAnnotation(annotation) {
+ this.model.addAnnotation(annotation);
+ },
+
+ removeAnnotation(annotation) {
+ this.model.removeAnnotation(annotation);
+ },
+
+ updateAnnotations_() {
+ this.annotationViews_ = [];
+ const annotations = this.model_.getAllAnnotations();
+ for (let i = 0; i < annotations.length; i++) {
+ this.annotationViews_.push(
+ annotations[i].getOrCreateView(this.viewport_));
+ }
+ this.invalidateDrawingContainer();
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (!this.model_) return;
+
+ const tracks = this.children;
+ for (let i = 0; i < tracks.length; ++i) {
+ tracks[i].addEventsToTrackMap(eventToTrackMap);
+ }
+
+ if (this.instantEvents === undefined) return;
+
+ const vp = this.viewport_;
+ this.instantEvents.forEach(function(ev) {
+ eventToTrackMap.addEvent(ev, this);
+ }.bind(this));
+ },
+
+ appendDeviceTrack_() {
+ const device = this.model.device;
+ const track = new tr.ui.tracks.DeviceTrack(this.viewport);
+ track.device = this.model.device;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendKernelTrack_() {
+ const kernel = this.model.kernel;
+ const track = new tr.ui.tracks.KernelTrack(this.viewport);
+ track.kernel = this.model.kernel;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendCpuUsageTrack_() {
+ const track = new tr.ui.tracks.CpuUsageTrack(this.viewport);
+ track.initialize(this.model);
+ if (!track.hasVisibleContent) return;
+
+ this.appendChild(track);
+ },
+
+ appendMemoryTrack_() {
+ const track = new tr.ui.tracks.MemoryTrack(this.viewport);
+ track.initialize(this.model);
+ if (!track.hasVisibleContent) return;
+
+ Polymer.dom(this).appendChild(track);
+ },
+
+ drawTrack(type) {
+ const ctx = this.context();
+ if (!this.model_) return;
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ switch (type) {
+ case tr.ui.tracks.DrawType.GRID:
+ this.viewport.drawMajorMarkLines(ctx, viewHeight);
+ // The model is the only thing that draws grid lines.
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.FLOW_ARROWS:
+ if (this.model_.flowIntervalTree.size === 0) {
+ ctx.restore();
+ return;
+ }
+
+ this.drawFlowArrows_(viewLWorld, viewRWorld);
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.INSTANT_EVENT:
+ if (!this.model_.instantEvents ||
+ this.model_.instantEvents.length === 0) {
+ break;
+ }
+
+ tr.ui.b.drawInstantSlicesAsLines(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.model_.instantEvents,
+ 4);
+
+ break;
+
+ case tr.ui.tracks.DrawType.MARKERS:
+ if (!this.viewport.interestRange.isEmpty) {
+ this.viewport.interestRange.draw(
+ ctx, viewLWorld, viewRWorld, viewHeight);
+ this.viewport.interestRange.drawIndicators(
+ ctx, viewLWorld, viewRWorld);
+ }
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.HIGHLIGHTS:
+ this.drawVSyncHighlight(
+ ctx, dt, viewLWorld, viewRWorld, viewHeight);
+ ctx.restore();
+ return;
+
+ case tr.ui.tracks.DrawType.ANNOTATIONS:
+ for (let i = 0; i < this.annotationViews_.length; i++) {
+ this.annotationViews_[i].draw(ctx);
+ }
+ ctx.restore();
+ return;
+ }
+ ctx.restore();
+
+ tr.ui.tracks.ContainerTrack.prototype.drawTrack.call(this, type);
+ },
+
+ drawFlowArrows_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)';
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
+ ctx.lineWidth = 1;
+
+ const events =
+ this.model_.flowIntervalTree.findIntersection(viewLWorld, viewRWorld);
+
+ // When not showing flow events, show only highlighted/selected ones.
+ const onlyHighlighted = !this.viewport.showFlowEvents;
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ for (let i = 0; i < events.length; ++i) {
+ if (onlyHighlighted &&
+ events[i].selectionState !== SelectionState.SELECTED &&
+ events[i].selectionState !== SelectionState.HIGHLIGHTED) {
+ continue;
+ }
+ this.drawFlowArrow_(ctx, events[i], canvasBounds);
+ }
+ },
+
+ drawFlowArrow_(ctx, flowEvent, canvasBounds) {
+ const dt = this.viewport.currentDisplayTransform;
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const startTrack = this.viewport.trackForEvent(flowEvent.startSlice);
+ const endTrack = this.viewport.trackForEvent(flowEvent.endSlice);
+
+ // TODO(nduca): Figure out how to draw flow arrows even when
+ // processes are collapsed, bug #931.
+ if (startTrack === undefined || endTrack === undefined) return;
+
+ const startBounds = startTrack.getBoundingClientRect();
+ const endBounds = endTrack.getBoundingClientRect();
+
+ if (flowEvent.selectionState === SelectionState.SELECTED) {
+ ctx.shadowBlur = 1;
+ ctx.shadowColor = 'red';
+ ctx.shadowOffsety = 2;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[
+ tr.b.ColorScheme.getVariantColorId(
+ flowEvent.colorId,
+ tr.b.ColorScheme.properties.brightenedOffsets[0])];
+ } else if (flowEvent.selectionState === SelectionState.HIGHLIGHTED) {
+ ctx.shadowBlur = 1;
+ ctx.shadowColor = 'red';
+ ctx.shadowOffsety = 2;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[
+ tr.b.ColorScheme.getVariantColorId(
+ flowEvent.colorId,
+ tr.b.ColorScheme.properties.brightenedOffsets[0])];
+ } else if (flowEvent.selectionState === SelectionState.DIMMED) {
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetX = 0;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[flowEvent.colorId];
+ } else {
+ let hasBoost = false;
+ const startSlice = flowEvent.startSlice;
+ hasBoost |= startSlice.selectionState === SelectionState.SELECTED;
+ hasBoost |= startSlice.selectionState === SelectionState.HIGHLIGHTED;
+ const endSlice = flowEvent.endSlice;
+ hasBoost |= endSlice.selectionState === SelectionState.SELECTED;
+ hasBoost |= endSlice.selectionState === SelectionState.HIGHLIGHTED;
+ if (hasBoost) {
+ ctx.shadowBlur = 1;
+ ctx.shadowColor = 'rgba(255, 0, 0, 0.4)';
+ ctx.shadowOffsety = 2;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[
+ tr.b.ColorScheme.getVariantColorId(
+ flowEvent.colorId,
+ tr.b.ColorScheme.properties.brightenedOffsets[0])];
+ } else {
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetX = 0;
+ ctx.strokeStyle = tr.b.ColorScheme.colorsAsStrings[flowEvent.colorId];
+ }
+ }
+
+ const startSize = startBounds.left + startBounds.top +
+ startBounds.bottom + startBounds.right;
+ const endSize = endBounds.left + endBounds.top +
+ endBounds.bottom + endBounds.right;
+ // Nothing to do if both ends of the track are collapsed.
+ if (startSize === 0 && endSize === 0) return;
+
+ const startY = this.calculateTrackY_(startTrack, canvasBounds);
+ const endY = this.calculateTrackY_(endTrack, canvasBounds);
+
+ const pixelStartY = pixelRatio * startY;
+ const pixelEndY = pixelRatio * endY;
+
+ const startXView = dt.xWorldToView(flowEvent.start);
+ const endXView = dt.xWorldToView(flowEvent.end);
+ const midXView = (startXView + endXView) / 2;
+
+ ctx.beginPath();
+ ctx.moveTo(startXView, pixelStartY);
+ ctx.bezierCurveTo(
+ midXView, pixelStartY,
+ midXView, pixelEndY,
+ endXView, pixelEndY);
+ ctx.stroke();
+
+ const arrowWidth = 5 * pixelRatio;
+ const distance = endXView - startXView;
+ if (distance <= (2 * arrowWidth)) return;
+
+ const tipX = endXView;
+ const tipY = pixelEndY;
+ const arrowHeight = (endBounds.height / 4) * pixelRatio;
+ tr.ui.b.drawTriangle(ctx,
+ tipX, tipY,
+ tipX - arrowWidth, tipY - arrowHeight,
+ tipX - arrowWidth, tipY + arrowHeight);
+ ctx.fill();
+ },
+
+ /**
+ * Highlights VSync events on the model track (using "zebra" striping).
+ */
+ drawVSyncHighlight(ctx, dt, viewLWorld, viewRWorld, viewHeight) {
+ if (!this.viewport_.highlightVSync) {
+ return;
+ }
+
+ const stripes = ModelTrack.generateStripes_(
+ this.vSyncTimes_, viewLWorld, viewRWorld);
+ if (stripes.length === 0) {
+ return;
+ }
+
+ const vSyncHighlightColor =
+ new tr.b.Color(ColorScheme.getColorForReservedNameAsString(
+ 'vsync_highlight_color'));
+
+ const stripeRange = stripes[stripes.length - 1].max - stripes[0].min;
+ const stripeDensity =
+ stripeRange ? stripes.length / (dt.scaleX * stripeRange) : 0;
+ const clampedStripeDensity =
+ tr.b.math.clamp(stripeDensity, ModelTrack.VSYNC_DENSITY_OPAQUE,
+ ModelTrack.VSYNC_DENSITY_TRANSPARENT);
+ const opacity =
+ (ModelTrack.VSYNC_DENSITY_TRANSPARENT - clampedStripeDensity) /
+ ModelTrack.VSYNC_DENSITY_RANGE;
+ if (opacity === 0) {
+ return;
+ }
+
+ ctx.fillStyle = vSyncHighlightColor.toStringWithAlphaOverride(
+ ModelTrack.VSYNC_HIGHLIGHT_ALPHA * opacity);
+
+ for (let i = 0; i < stripes.length; i++) {
+ const xLeftView = dt.xWorldToView(stripes[i].min);
+ const xRightView = dt.xWorldToView(stripes[i].max);
+ ctx.fillRect(xLeftView, 0, xRightView - xLeftView, viewHeight);
+ }
+ },
+
+ calculateTrackY_(track, canvasBounds) {
+ const bounds = track.getBoundingClientRect();
+ const size = bounds.left + bounds.top + bounds.bottom + bounds.right;
+ if (size === 0) {
+ return this.calculateTrackY_(
+ Polymer.dom(track).parentNode, canvasBounds);
+ }
+
+ return bounds.top - canvasBounds.top + (bounds.height / 2);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onPickHit(instantEvent) {
+ selection.push(instantEvent);
+ }
+ const instantEventWidth = 3 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.model_.instantEvents,
+ function(x) { return x.start; },
+ function(x) { return x.duration + instantEventWidth; },
+ loWX, hiWX,
+ onPickHit.bind(this));
+
+ tr.ui.tracks.ContainerTrack.prototype.
+ addIntersectingEventsInRangeToSelectionInWorldSpace.
+ apply(this, arguments);
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.addClosestInstantEventToSelection(this.model_.instantEvents,
+ worldX, worldMaxDist, selection);
+ tr.ui.tracks.ContainerTrack.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ }
+ };
+
+ return {
+ ModelTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html
new file mode 100644
index 00000000000..53e19146ae2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/model_track_test.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/thread.html">
+<link rel="import" href="/tracing/ui/tracks/model_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Range = tr.b.math.Range;
+ const VIEW_L_WORLD = 100;
+ const VIEW_R_WORLD = 1000;
+
+ function testGenerateStripes(times, expectedRanges) {
+ const ranges = tr.ui.tracks.ModelTrack.generateStripes_(
+ times, VIEW_L_WORLD, VIEW_R_WORLD);
+
+ assert.sameDeepMembers(ranges, expectedRanges);
+ }
+
+ test('generateStripesInside', function() {
+ const range200To500 = Range.fromExplicitRange(200, 500);
+ const range800To900 = Range.fromExplicitRange(800, 900);
+ const range998To999 = Range.fromExplicitRange(998, 999);
+ testGenerateStripes([], []);
+ testGenerateStripes([200, 500], [range200To500]);
+ testGenerateStripes([200, 500, 800, 900], [range200To500, range800To900]);
+ testGenerateStripes(
+ [200, 500, 800, 900, 998, 999],
+ [range200To500, range800To900, range998To999]);
+ });
+
+ test('generateStripesOutside', function() {
+ const range101To999 = Range.fromExplicitRange(101, 999);
+ // Far left.
+ testGenerateStripes([0, 99], []);
+ testGenerateStripes([0, 10, 50, 99], []);
+ testGenerateStripes([0, 99, 101, 999], [range101To999]);
+ testGenerateStripes([0, 10, 50, 99, 101, 999], [range101To999]);
+
+ // Far right.
+ testGenerateStripes([1001, 2000], []);
+ testGenerateStripes([1001, 2000, 3000, 4000], []);
+ testGenerateStripes([101, 999, 1001, 2000], [range101To999]);
+ testGenerateStripes([101, 999, 1001, 2000, 3000, 4000], [range101To999]);
+
+ // Far both.
+ testGenerateStripes([0, 99, 1001, 2000], []);
+ testGenerateStripes([0, 10, 50, 99, 1001, 2000], []);
+ testGenerateStripes([0, 10, 50, 99, 1001, 2000, 3000, 4000], []);
+ testGenerateStripes([0, 99, 101, 999, 1001, 2000], [range101To999]);
+ });
+
+ test('generateStripesOverlap', function() {
+ const rangeLeftWorldTo101 = Range.fromExplicitRange(VIEW_L_WORLD, 101);
+ const range102To103 = Range.fromExplicitRange(102, 103);
+ const range200To900 = Range.fromExplicitRange(200, 900);
+ const range997To998 = Range.fromExplicitRange(997, 998);
+ const range999ToRightWorld = Range.fromExplicitRange(999, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+
+
+ // Left overlap.
+ testGenerateStripes([0, 101], [rangeLeftWorldTo101]);
+ testGenerateStripes([0, 1, 2, 101], [rangeLeftWorldTo101]);
+ testGenerateStripes(
+ [2, 101, 102, 103],
+ [rangeLeftWorldTo101, range102To103]);
+ testGenerateStripes(
+ [0, 1, 2, 101, 102, 103],
+ [rangeLeftWorldTo101, range102To103]);
+ testGenerateStripes(
+ [0, 1, 2, 101, 102, 103, 1001, 3000],
+ [rangeLeftWorldTo101, range102To103]);
+
+ // Right overlap.
+ testGenerateStripes([999, 2000], [range999ToRightWorld]);
+ testGenerateStripes([999, 2000, 3000, 4000], [range999ToRightWorld]);
+ testGenerateStripes(
+ [997, 998, 999, 2000],
+ [range997To998, range999ToRightWorld]);
+ testGenerateStripes(
+ [997, 998, 999, 2000, 3000, 4000],
+ [range997To998, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 997, 998, 999, 2000, 3000, 4000],
+ [range997To998, range999ToRightWorld]);
+
+ // Both overlap.
+ testGenerateStripes([0, 2000], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes(
+ [0, 101, 999, 2000],
+ [rangeLeftWorldTo101, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 101, 200, 900, 999, 2000],
+ [rangeLeftWorldTo101, range200To900, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 90, 101, 999, 2000, 3000, 4000],
+ [rangeLeftWorldTo101, range999ToRightWorld]);
+ testGenerateStripes(
+ [0, 10, 90, 101, 200, 900, 999, 2000, 3000, 4000],
+ [rangeLeftWorldTo101, range200To900, range999ToRightWorld]);
+ });
+
+ test('generateStripesOdd', function() {
+ const range500To900 = Range.fromExplicitRange(500, 900);
+ const rangeLeftWorldTo200 = Range.fromExplicitRange(VIEW_L_WORLD, 200);
+ const rangeLeftWorldTo500 = Range.fromExplicitRange(VIEW_L_WORLD, 500);
+ const range500ToRightWorld = Range.fromExplicitRange(500, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+
+ // One VSync.
+ testGenerateStripes([0], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes([500], [range500ToRightWorld]);
+ testGenerateStripes([1500], []);
+
+ // Multiple VSyncs.
+ testGenerateStripes([0, 10, 20], [rangeLeftWorldToRightWorld]);
+ testGenerateStripes([0, 500, 2000], [rangeLeftWorldTo500]);
+ testGenerateStripes([0, 10, 500], [range500ToRightWorld]);
+ testGenerateStripes([0, 10, 2000], []);
+ testGenerateStripes(
+ [0, 200, 500],
+ [rangeLeftWorldTo200, range500ToRightWorld]);
+ testGenerateStripes(
+ [0, 200, 500, 900],
+ [rangeLeftWorldTo200, range500To900]);
+ });
+
+ test('generateStripesBorder', function() {
+ const rangeLeftWorldToLeftWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_L_WORLD);
+ const rangeRightWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_R_WORLD, VIEW_R_WORLD);
+ const rangeLeftWorldToRightWorld =
+ Range.fromExplicitRange(VIEW_L_WORLD, VIEW_R_WORLD);
+ const rangeLeftWorldTo200 = Range.fromExplicitRange(VIEW_L_WORLD, 200);
+ const range200To500 = Range.fromExplicitRange(200, 500);
+ const range500ToRightWorld = Range.fromExplicitRange(500, VIEW_R_WORLD);
+ testGenerateStripes([0, VIEW_L_WORLD], [rangeLeftWorldToLeftWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, VIEW_L_WORLD],
+ [rangeLeftWorldToLeftWorld]);
+ testGenerateStripes(
+ [VIEW_R_WORLD, 2000],
+ [rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_R_WORLD, VIEW_R_WORLD],
+ [rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, VIEW_R_WORLD],
+ [rangeLeftWorldToRightWorld]);
+ testGenerateStripes(
+ [VIEW_L_WORLD, 200, 500, VIEW_R_WORLD],
+ [rangeLeftWorldTo200, range500ToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, VIEW_L_WORLD, 200, 500, VIEW_R_WORLD, 2000],
+ [rangeLeftWorldToLeftWorld, range200To500,
+ rangeRightWorldToRightWorld]);
+ testGenerateStripes(
+ [0, 10, VIEW_L_WORLD, VIEW_R_WORLD, 2000, 3000],
+ [rangeLeftWorldToRightWorld]);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html
new file mode 100644
index 00000000000..8b5fd837f0d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/multi_row_track.html
@@ -0,0 +1,240 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/model_settings.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a group of objects in multiple rows.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const MultiRowTrack = tr.ui.b.define(
+ 'multi-row-track', tr.ui.tracks.ContainerTrack);
+
+ MultiRowTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.tooltip_ = '';
+ this.heading_ = '';
+
+ this.groupingSource_ = undefined;
+ this.itemsToGroup_ = undefined;
+
+ this.defaultToCollapsedWhenSubRowCountMoreThan = 1;
+
+ this.currentSubRowsWithHeadings_ = undefined;
+ this.expanded_ = true;
+ },
+
+ get itemsToGroup() {
+ return this.itemsToGroup_;
+ },
+
+ setItemsToGroup(itemsToGroup, opt_groupingSource) {
+ this.itemsToGroup_ = itemsToGroup;
+ this.groupingSource_ = opt_groupingSource;
+ this.currentSubRowsWithHeadings_ = undefined;
+ this.updateContents_();
+ this.updateExpandedStateFromGroupingSource_();
+ },
+
+ /**
+ * Opt-out from using buildSubRows_() and provide prebuilt rows.
+ * Array of {row: [rowItems...], heading} dicts is expected as an argument.
+ */
+ setPrebuiltSubRows(groupingSource, subRowsWithHeadings) {
+ this.itemsToGroup_ = undefined;
+ this.groupingSource_ = groupingSource;
+ this.currentSubRowsWithHeadings_ = subRowsWithHeadings;
+ this.updateContents_();
+ this.updateExpandedStateFromGroupingSource_();
+ },
+
+ get heading() {
+ return this.heading_;
+ },
+
+ set heading(h) {
+ this.heading_ = h;
+ this.updateHeadingAndTooltip_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(t) {
+ this.tooltip_ = t;
+ this.updateHeadingAndTooltip_();
+ },
+
+ get subRows() {
+ return this.currentSubRowsWithHeadings_.map(elem => elem.row);
+ },
+
+ get hasVisibleContent() {
+ return this.children.length > 0;
+ },
+
+ get expanded() {
+ return this.expanded_;
+ },
+
+ set expanded(expanded) {
+ if (this.expanded_ === expanded) return;
+
+ this.expanded_ = expanded;
+ this.expandedStateChanged_();
+ },
+
+ onHeadingClicked_(e) {
+ if (this.subRows.length <= 1) return;
+
+ this.expanded = !this.expanded;
+
+ if (this.groupingSource_) {
+ const modelSettings = new tr.model.ModelSettings(
+ this.groupingSource_.model);
+ modelSettings.setSettingFor(this.groupingSource_, 'expanded',
+ this.expanded);
+ }
+
+ e.stopPropagation();
+ },
+
+ updateExpandedStateFromGroupingSource_() {
+ if (this.groupingSource_) {
+ const numSubRows = this.subRows.length;
+ const modelSettings = new tr.model.ModelSettings(
+ this.groupingSource_.model);
+ if (numSubRows > 1) {
+ let defaultExpanded;
+ if (numSubRows > this.defaultToCollapsedWhenSubRowCountMoreThan) {
+ defaultExpanded = false;
+ } else {
+ defaultExpanded = true;
+ }
+ this.expanded = modelSettings.getSettingFor(
+ this.groupingSource_, 'expanded', defaultExpanded);
+ } else {
+ this.expanded = undefined;
+ }
+ }
+ },
+
+ expandedStateChanged_() {
+ const minH = Math.max(2, Math.ceil(18 / this.children.length));
+ const h = (this.expanded_ ? 18 : minH) + 'px';
+
+ for (let i = 0; i < this.children.length; i++) {
+ this.children[i].height = h;
+ if (i === 0) {
+ this.children[i].arrowVisible = true;
+ }
+ this.children[i].expanded = this.expanded;
+ }
+
+ if (this.children.length === 1) {
+ this.children[0].expanded = true;
+ this.children[0].arrowVisible = false;
+ }
+ },
+
+ updateContents_() {
+ tr.ui.tracks.ContainerTrack.prototype.updateContents_.call(this);
+ this.detach(); // Clear sub-tracks.
+
+ if (this.currentSubRowsWithHeadings_ === undefined) {
+ // No prebuilt rows, build it.
+ if (this.itemsToGroup_ === undefined) {
+ return;
+ }
+ const subRows = this.buildSubRows_(this.itemsToGroup_);
+ this.currentSubRowsWithHeadings_ = subRows.map(row => {
+ return {row, heading: undefined};
+ });
+ }
+ if (this.currentSubRowsWithHeadings_ === undefined ||
+ this.currentSubRowsWithHeadings_.length === 0) {
+ return;
+ }
+
+ const addSubTrackEx = (items, opt_heading) => {
+ const track = this.addSubTrack_(items);
+ if (opt_heading !== undefined) {
+ track.heading = opt_heading;
+ }
+ track.addEventListener(
+ 'heading-clicked', this.onHeadingClicked_.bind(this));
+ };
+
+ if (this.currentSubRowsWithHeadings_[0].heading !== undefined &&
+ this.currentSubRowsWithHeadings_[0].heading !== this.heading_) {
+ // Create an empty row to render the group's title there.
+ addSubTrackEx([]);
+ }
+
+ for (const subRowWithHeading of this.currentSubRowsWithHeadings_) {
+ const subRow = subRowWithHeading.row;
+ if (subRow.length === 0) {
+ continue;
+ }
+ addSubTrackEx(subRow, subRowWithHeading.heading);
+ }
+
+ this.updateHeadingAndTooltip_();
+ this.expandedStateChanged_();
+ },
+
+ updateHeadingAndTooltip_() {
+ if (!Polymer.dom(this).firstChild) return;
+
+ Polymer.dom(this).firstChild.heading = this.heading_;
+ Polymer.dom(this).firstChild.tooltip = this.tooltip_;
+ },
+
+ /**
+ * Breaks up the list of slices into N rows, each of which is a list of
+ * slices that are non overlapping.
+ */
+ buildSubRows_(itemsToGroup) {
+ throw new Error('Not implemented');
+ },
+
+ addSubTrack_(subRowItems) {
+ throw new Error('Not implemented');
+ },
+
+ areArrayContentsSame_(a, b) {
+ if (!a || !b) return false;
+
+ if (!a.length || !b.length) return false;
+
+ if (a.length !== b.length) return false;
+
+ for (let i = 0; i < a.length; ++i) {
+ if (a[i] !== b[i]) return false;
+ }
+ return true;
+ }
+ };
+
+ return {
+ MultiRowTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html
new file mode 100644
index 00000000000..b97b48c3ef0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_group_track.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/analysis/object_instance_view.html">
+<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a ObjectInstanceGroup.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const ObjectInstanceGroupTrack = tr.ui.b.define(
+ 'object-instance-group-track', tr.ui.tracks.MultiRowTrack);
+
+ ObjectInstanceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('object-instance-group-track');
+ this.objectInstances_ = undefined;
+ },
+
+ get objectInstances() {
+ return this.itemsToGroup;
+ },
+
+ set objectInstances(objectInstances) {
+ this.setItemsToGroup(objectInstances);
+ },
+
+ addSubTrack_(objectInstances) {
+ const hasMultipleRows = this.subRows.length > 1;
+ const track = new tr.ui.tracks.ObjectInstanceTrack(this.viewport);
+ track.objectInstances = objectInstances;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ },
+
+ buildSubRows_(objectInstances) {
+ objectInstances.sort(function(x, y) {
+ return x.creationTs - y.creationTs;
+ });
+
+ const subRows = [];
+ for (let i = 0; i < objectInstances.length; i++) {
+ const objectInstance = objectInstances[i];
+
+ let found = false;
+ for (let j = 0; j < subRows.length; j++) {
+ const subRow = subRows[j];
+ const lastItemInSubRow = subRow[subRow.length - 1];
+ if (objectInstance.creationTs >= lastItemInSubRow.deletionTs) {
+ found = true;
+ subRow.push(objectInstance);
+ break;
+ }
+ }
+ if (!found) {
+ subRows.push([objectInstance]);
+ }
+ }
+ return subRows;
+ },
+ updateHeadingAndTooltip_() {
+ }
+ };
+
+ return {
+ ObjectInstanceGroupTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css
new file mode 100644
index 00000000000..0919e85524e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.css
@@ -0,0 +1,8 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.object-instance-track {
+ height: 18px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html
new file mode 100644
index 00000000000..f21a87d04db
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track.html
@@ -0,0 +1,294 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/object_instance_track.css">
+
+<link rel="import" href="/tracing/base/extension_registry.html">
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/event.html">
+<link rel="import" href="/tracing/ui/base/event_presenter.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const SelectionState = tr.model.SelectionState;
+ const EventPresenter = tr.ui.b.EventPresenter;
+
+ /**
+ * A track that displays an array of Slice objects.
+ * @constructor
+ * @extends {Track}
+ */
+ const ObjectInstanceTrack = tr.ui.b.define(
+ 'object-instance-track', tr.ui.tracks.Track);
+
+ ObjectInstanceTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('object-instance-track');
+ this.objectInstances_ = [];
+ this.objectSnapshots_ = [];
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ get objectInstances() {
+ return this.objectInstances_;
+ },
+
+ set objectInstances(objectInstances) {
+ if (!objectInstances || objectInstances.length === 0) {
+ this.heading = '';
+ this.objectInstances_ = [];
+ this.objectSnapshots_ = [];
+ return;
+ }
+ this.heading = objectInstances[0].baseTypeName;
+ this.objectInstances_ = objectInstances;
+ this.objectSnapshots_ = [];
+ this.objectInstances_.forEach(function(instance) {
+ this.objectSnapshots_.push.apply(
+ this.objectSnapshots_, instance.snapshots);
+ }, this);
+ this.objectSnapshots_.sort(function(a, b) {
+ return a.ts - b.ts;
+ });
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ get snapshotRadiusView() {
+ return 7 * (window.devicePixelRatio || 1);
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawObjectInstances_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawObjectInstances_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const bounds = this.getBoundingClientRect();
+ const height = bounds.height * pixelRatio;
+ const halfHeight = height * 0.5;
+ const twoPi = Math.PI * 2;
+
+ // Culling parameters.
+ const dt = this.viewport.currentDisplayTransform;
+ const snapshotRadiusView = this.snapshotRadiusView;
+ const snapshotRadiusWorld = dt.xViewVectorToWorld(height);
+
+ // Instances
+ const objectInstances = this.objectInstances_;
+ let loI = tr.b.findLowIndexInSortedArray(
+ objectInstances,
+ function(instance) {
+ return instance.deletionTs;
+ },
+ viewLWorld);
+ ctx.save();
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ for (let i = loI; i < objectInstances.length; ++i) {
+ const instance = objectInstances[i];
+ const x = instance.creationTs;
+ if (x > viewRWorld) break;
+
+ const right = instance.deletionTs === Number.MAX_VALUE ?
+ viewRWorld : instance.deletionTs;
+ const xView = dt.xWorldToView(x);
+ const widthView = dt.xWorldVectorToView(right - x);
+ ctx.fillStyle = EventPresenter.getObjectInstanceColor(instance);
+ ctx.fillRect(xView, pixelRatio, widthView, height - 2 * pixelRatio);
+ }
+ ctx.restore();
+
+ // Snapshots. Has to run in worldspace because ctx.arc gets transformed.
+ const objectSnapshots = this.objectSnapshots_;
+ loI = tr.b.findLowIndexInSortedArray(
+ objectSnapshots,
+ function(snapshot) {
+ return snapshot.ts + snapshotRadiusWorld;
+ },
+ viewLWorld);
+ for (let i = loI; i < objectSnapshots.length; ++i) {
+ const snapshot = objectSnapshots[i];
+ const x = snapshot.ts;
+ if (x - snapshotRadiusWorld > viewRWorld) break;
+
+ const xView = dt.xWorldToView(x);
+
+ ctx.fillStyle = EventPresenter.getObjectSnapshotColor(snapshot);
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, snapshotRadiusView, 0, twoPi);
+ ctx.fill();
+ if (snapshot.selected) {
+ ctx.lineWidth = 5;
+ ctx.strokeStyle = 'rgb(100,100,0)';
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(xView, halfHeight, snapshotRadiusView - 1, 0, twoPi);
+ ctx.lineWidth = 2;
+ ctx.strokeStyle = 'rgb(255,255,0)';
+ ctx.stroke();
+ } else {
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.stroke();
+ }
+ }
+ ctx.lineWidth = 1;
+
+ // For performance reasons we only check the SelectionState of the first
+ // instance. If it's DIMMED we assume that all are DIMMED.
+ // TODO(egraether): Allow partial highlight.
+ let selectionState = SelectionState.NONE;
+ if (objectInstances.length &&
+ objectInstances[0].selectionState === SelectionState.DIMMED) {
+ selectionState = SelectionState.DIMMED;
+ }
+
+ // Dim the track when there is an active highlight.
+ if (selectionState === SelectionState.DIMMED) {
+ const width = bounds.width * pixelRatio;
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
+ ctx.fillRect(0, 0, width, height);
+ ctx.restore();
+ }
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.objectInstance_ !== undefined) {
+ this.objectInstance_.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ }
+
+ if (this.objectSnapshots_ !== undefined) {
+ this.objectSnapshots_.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ }
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ // Pick snapshots first.
+ let foundSnapshot = false;
+ function onSnapshot(snapshot) {
+ selection.push(snapshot);
+ foundSnapshot = true;
+ }
+ const snapshotRadiusView = this.snapshotRadiusView;
+ const snapshotRadiusWorld = viewPixWidthWorld * snapshotRadiusView;
+ tr.b.iterateOverIntersectingIntervals(
+ this.objectSnapshots_,
+ function(x) { return x.ts - snapshotRadiusWorld; },
+ function(x) { return 2 * snapshotRadiusWorld; },
+ loWX, hiWX,
+ onSnapshot);
+ if (foundSnapshot) return;
+
+ // Try picking instances.
+ tr.b.iterateOverIntersectingIntervals(
+ this.objectInstances_,
+ function(x) { return x.creationTs; },
+ function(x) { return x.deletionTs - x.creationTs; },
+ loWX, hiWX,
+ (value) => { selection.push(value); });
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {event} The current event item.
+ * @param {Number} offset Number of slices away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ let events;
+ if (event instanceof tr.model.ObjectSnapshot) {
+ events = this.objectSnapshots_;
+ } else if (event instanceof tr.model.ObjectInstance) {
+ events = this.objectInstances_;
+ } else {
+ throw new Error('Unrecognized event');
+ }
+
+ const index = events.indexOf(event);
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < events.length) {
+ selection.push(events[newIndex]);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const snapshot = tr.b.findClosestElementInSortedArray(
+ this.objectSnapshots_,
+ function(x) { return x.ts; },
+ worldX,
+ worldMaxDist);
+
+ if (!snapshot) return;
+
+ selection.push(snapshot);
+
+ // TODO(egraether): Search for object instances as well, which was not
+ // implemented because it makes little sense with the current visual and
+ // needs to take care of overlapping intervals.
+ }
+ };
+
+
+ const options = new tr.b.ExtensionRegistryOptions(
+ tr.b.TYPE_BASED_REGISTRY_MODE);
+ tr.b.decorateExtensionRegistry(ObjectInstanceTrack, options);
+
+ return {
+ ObjectInstanceTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html
new file mode 100644
index 00000000000..8312d0ba7e5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/object_instance_track_test.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/object_collection.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+<link rel="import" href="/tracing/model/selection_state.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const ObjectInstanceTrack = tr.ui.tracks.ObjectInstanceTrack;
+ const Viewport = tr.ui.TimelineViewport;
+
+ const createObjects = function() {
+ const objects = new tr.model.ObjectCollection({});
+ const scopedId1 = new tr.model.ScopedId('ptr', '0x1000');
+ objects.idWasCreated(scopedId1, 'tr.e.cc', 'Frame', 10);
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 10, 'snapshot-1');
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 25, 'snapshot-2');
+ objects.addSnapshot(scopedId1, 'tr.e.cc', 'Frame', 40, 'snapshot-3');
+ objects.idWasDeleted(scopedId1, 'tr.e.cc', 'Frame', 45);
+
+ const scopedId2 = new tr.model.ScopedId('ptr', '0x1001');
+ objects.idWasCreated(scopedId2, 'skia', 'Picture', 20);
+ objects.addSnapshot(scopedId2, 'skia', 'Picture', 20, 'snapshot-1');
+ objects.idWasDeleted(scopedId2, 'skia', 'Picture', 25);
+ return objects;
+ };
+
+ test('instantiate', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+ frames[0].snapshots[1].selectionState =
+ tr.model.SelectionState.SELECTED;
+
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = ObjectInstanceTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasic';
+ track.objectInstances = frames;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('selectionHitTestingWithThreadTrack', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+
+ const track = ObjectInstanceTrack(new Viewport());
+ track.objectInstances = frames;
+
+ // Hit outside range
+ let selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 8, 8.1, 0.1, selection);
+ assert.strictEqual(selection.length, 0);
+
+ // Hit the first snapshot, via pixel-nearness.
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 9.98, 9.99, 0.1, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.instanceOf(tr.b.getOnlyElement(selection), tr.model.ObjectSnapshot);
+
+ // Hit the instance, between the 1st and 2nd snapshots
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ 20, 20.1, 0.1, selection);
+ assert.strictEqual(selection.length, 1);
+ assert.instanceOf(tr.b.getOnlyElement(selection), tr.model.ObjectInstance);
+ });
+
+ test('addEventNearToProvidedEventToSelection', function() {
+ const objects = createObjects();
+ const frames = objects.getAllInstancesByTypeName().Frame;
+
+ const track = ObjectInstanceTrack(new Viewport());
+ track.objectInstances = frames;
+
+ const instance = new tr.model.ObjectInstance(
+ {}, new tr.model.ScopedId('ptr', '0x1000'), 'cat', 'n', 10);
+
+ assert.doesNotThrow(function() {
+ track.addEventNearToProvidedEventToSelection(instance, 0, undefined);
+ });
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html
new file mode 100644
index 00000000000..e43bce0cec2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/other_threads_track.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+Copyright 2016 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays threads with only scheduling information but no
+ * slices. By default it's collapsed to minimize initial visual difference
+ * while allowing the user to drill-down into whatever process is
+ * interesting to them.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const OtherThreadsTrack = tr.ui.b.define(
+ 'other-threads-track', tr.ui.tracks.OtherThreadsTrack);
+
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ OtherThreadsTrack.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+
+ this.header_ = document.createElement('tr-ui-b-heading');
+ this.header_.addEventListener('click', this.onHeaderClick_.bind(this));
+ this.header_.heading = 'Other Threads';
+ this.header_.tooltip = 'Threads with only scheduling information';
+ this.header_.arrowVisible = true;
+
+ this.threads_ = [];
+ this.expanded = false;
+ this.collapsible_ = true;
+ },
+
+ set threads(threads) {
+ this.threads_ = threads;
+ this.updateContents_();
+ },
+
+ set collapsible(collapsible) {
+ this.collapsible_ = collapsible;
+ this.updateContents_();
+ },
+
+ onHeaderClick_(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.expanded = !this.expanded;
+ },
+
+ get expanded() {
+ return this.header_.expanded;
+ },
+
+ set expanded(expanded) {
+ expanded = !!expanded;
+
+ if (this.expanded === expanded) return;
+
+ this.header_.expanded = expanded;
+
+ // Expanding and collapsing tracks is, essentially, growing and shrinking
+ // the viewport. We dispatch a change event to trigger any processing
+ // to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.detach();
+ if (this.collapsible_) {
+ Polymer.dom(this).appendChild(this.header_);
+ }
+ if (this.expanded || !this.collapsible_) {
+ for (const thread of this.threads_) {
+ const track = new tr.ui.tracks.ThreadTrack(this.viewport);
+ track.thread = thread;
+ if (!track.hasVisibleContent) return;
+
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ }
+ }
+ };
+
+ return {
+ OtherThreadsTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html
new file mode 100644
index 00000000000..d32cd21e9a3
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/chart_point.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series.html">
+<link rel="import" href="/tracing/ui/tracks/chart_series_y_axis.html">
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+
+<style>
+.power-series-track {
+ height: 90px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+ const ChartTrack = tr.ui.tracks.ChartTrack;
+
+ /**
+ * A track that displays a PowerSeries.
+ *
+ * @constructor
+ * @extends {ChartTrack}
+ */
+ const PowerSeriesTrack = tr.ui.b.define('power-series-track', ChartTrack);
+
+ PowerSeriesTrack.prototype = {
+ __proto__: ChartTrack.prototype,
+
+ decorate(viewport) {
+ ChartTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('power-series-track');
+ this.heading = 'Power';
+ this.powerSeries_ = undefined;
+ },
+
+ set powerSeries(powerSeries) {
+ this.powerSeries_ = powerSeries;
+
+ this.series = this.buildChartSeries_();
+ this.autoSetAllAxes({expandMax: true});
+ },
+
+ get hasVisibleContent() {
+ return (this.powerSeries_ && this.powerSeries_.samples.length > 0);
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ containerToTrackMap.addContainer(this.powerSeries_, this);
+ },
+
+ buildChartSeries_() {
+ if (!this.hasVisibleContent) return [];
+
+ const seriesYAxis = new tr.ui.tracks.ChartSeriesYAxis(0, undefined);
+ const pts = this.powerSeries_.samples.map(function(smpl) {
+ return new tr.ui.tracks.ChartPoint(smpl, smpl.start, smpl.powerInW);
+ });
+ const renderingConfig = {
+ chartType: tr.ui.tracks.ChartSeriesType.AREA,
+ colorId: ColorScheme.getColorIdForGeneralPurposeString(this.heading)
+ };
+
+ return [new tr.ui.tracks.ChartSeries(pts, seriesYAxis, renderingConfig)];
+ }
+ };
+
+ return {
+ PowerSeriesTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html
new file mode 100644
index 00000000000..9e8b03aa168
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/power_series_track_test.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel='import' href='/tracing/model/device.html'>
+<link rel='import' href='/tracing/model/model.html'>
+<link rel='import' href='/tracing/model/power_series.html'>
+<link rel='import' href='/tracing/ui/base/constants.html'>
+<link rel='import' href='/tracing/ui/timeline_viewport.html'>
+<link rel='import' href='/tracing/ui/tracks/container_to_track_map.html'>
+<link rel='import' href='/tracing/ui/tracks/drawing_container.html'>
+<link rel="import" href="/tracing/ui/tracks/power_series_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Device = tr.model.Device;
+ const Model = tr.Model;
+ const PowerSeries = tr.model.PowerSeries;
+ const PowerSeriesTrack = tr.ui.tracks.PowerSeriesTrack;
+
+ const createDrawingContainer = function(series) {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ if (series) {
+ series.updateBounds();
+ setDisplayTransformFromBounds(viewport, series.bounds);
+ }
+
+ return drawingContainer;
+ };
+
+ /**
+ * Sets the mapping between the input range of timestamps and the output range
+ * of horizontal pixels.
+ */
+ const setDisplayTransformFromBounds = function(viewport, bounds) {
+ const dt = new tr.ui.TimelineDisplayTransform();
+ const pixelRatio = window.devicePixelRatio || 1;
+ const chartPixelWidth =
+ (window.innerWidth - tr.ui.b.constants.HEADING_WIDTH) * pixelRatio;
+ dt.xSetWorldBounds(bounds.min, bounds.max, chartPixelWidth);
+ viewport.setDisplayTransformImmediately(dt);
+ };
+
+ test('instantiate', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(0.5, 2);
+ series.addPowerSample(1, 3);
+ series.addPowerSample(1.5, 4);
+
+ const drawingContainer = createDrawingContainer(series);
+ const track = new PowerSeriesTrack(drawingContainer.viewport);
+ track.powerSeries = series;
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(drawingContainer);
+ });
+
+ test('hasVisibleContent_trueWithPowerSamplesPresent', function() {
+ const series = new PowerSeries(new Model().device);
+ series.addPowerSample(0, 1);
+ series.addPowerSample(0.5, 2);
+ series.addPowerSample(1, 3);
+ series.addPowerSample(1.5, 4);
+
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ track.powerSeries = series;
+
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithUndefinedPowerSeries', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ track.powerSeries = undefined;
+
+ assert.notOk(track.hasVisibleContent);
+ });
+
+ test('hasVisibleContent_falseWithEmptyPowerSeries', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const track = new PowerSeriesTrack(viewport);
+ const series = new PowerSeries(new Model().device);
+ track.powerSeries = series;
+
+ assert.notOk(track.hasVisibleContent);
+ });
+
+ test('addContainersToTrackMap', function() {
+ const div = document.createElement('div');
+ const viewport = new tr.ui.TimelineViewport(div);
+
+ const powerSeriesTrack = new PowerSeriesTrack(viewport);
+ const series = new PowerSeries(new Model().device);
+ powerSeriesTrack.powerSeries = series;
+
+ const containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
+ powerSeriesTrack.addContainersToTrackMap(containerToTrackMap);
+
+ assert.strictEqual(
+ containerToTrackMap.getTrackByStableId('Device.PowerSeries'),
+ powerSeriesTrack);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html
new file mode 100644
index 00000000000..247d707f58f
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/chart_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_util.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ALLOCATED_MEMORY_TRACK_HEIGHT = 50;
+
+ /**
+ * A track that displays an array of ProcessMemoryDump objects.
+ * @constructor
+ * @extends {ContainerTrack}
+ */
+ const ProcessMemoryDumpTrack = tr.ui.b.define(
+ 'process-memory-dump-track', tr.ui.tracks.ContainerTrack);
+
+ ProcessMemoryDumpTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ this.memoryDumps_ = undefined;
+ },
+
+ get memoryDumps() {
+ return this.memoryDumps_;
+ },
+
+ set memoryDumps(memoryDumps) {
+ this.memoryDumps_ = memoryDumps;
+ this.updateContents_();
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ // Show no tracks if there are no dumps.
+ if (!this.memoryDumps_ || !this.memoryDumps_.length) return;
+
+ this.appendAllocatedMemoryTrack_();
+ },
+
+ appendAllocatedMemoryTrack_() {
+ const series = tr.ui.tracks.buildProcessAllocatedMemoryChartSeries(
+ this.memoryDumps_);
+ if (!series) return;
+
+ const track = new tr.ui.tracks.ChartTrack(this.viewport);
+ track.heading = 'Memory per component';
+ track.height = ALLOCATED_MEMORY_TRACK_HEIGHT + 'px';
+ track.series = series;
+ track.autoSetAllAxes({expandMax: true});
+ Polymer.dom(this).appendChild(track);
+ }
+ };
+
+ return {
+ ProcessMemoryDumpTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html
new file mode 100644
index 00000000000..897d2883c62
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_memory_dump_track_test.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/memory_dump_track_test_utils.html">
+<link rel="import" href="/tracing/ui/tracks/process_memory_dump_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Viewport = tr.ui.TimelineViewport;
+ const ProcessMemoryDumpTrack = tr.ui.tracks.ProcessMemoryDumpTrack;
+ const createTestProcessMemoryDumps =
+ tr.ui.tracks.createTestProcessMemoryDumps;
+
+ function instantiateTrack(withVMRegions, withAllocatorDumps,
+ expectedTrackCount) {
+ const dumps = createTestProcessMemoryDumps(
+ withVMRegions, withAllocatorDumps);
+
+ const div = document.createElement('div');
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new ProcessMemoryDumpTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.invalidate();
+
+ track.memoryDumps = dumps;
+
+ // TODO(petrcermak): Check that the div has indeed zero size.
+ if (expectedTrackCount > 0) {
+ this.addHTMLOutput(div);
+ }
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 50, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ assert.lengthOf(track.tracks_, expectedTrackCount);
+ }
+
+ test('instantiate_withoutMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, false, 0);
+ });
+ test('instantiate_withMemoryAllocatorDumps', function() {
+ instantiateTrack.call(this, false, true, 1);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html
new file mode 100644
index 00000000000..c6560f40118
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/color_scheme.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ColorScheme = tr.b.ColorScheme;
+
+ /**
+ * Visualizes a Process's state using a series of rects to represent activity.
+ * @constructor
+ */
+ const ProcessSummaryTrack = tr.ui.b.define('process-summary-track',
+ tr.ui.tracks.RectTrack);
+
+ ProcessSummaryTrack.buildRectsFromProcess = function(process) {
+ if (!process) return [];
+
+ const ops = [];
+ // build list of start/end ops for each top level or important slice
+ const pushOp = function(isStart, time, slice) {
+ ops.push({
+ isStart,
+ time,
+ slice
+ });
+ };
+ for (const tid in process.threads) {
+ const sliceGroup = process.threads[tid].sliceGroup;
+
+ sliceGroup.topLevelSlices.forEach(function(slice) {
+ pushOp(true, slice.start, undefined);
+ pushOp(false, slice.end, undefined);
+ });
+ sliceGroup.slices.forEach(function(slice) {
+ if (slice.important) {
+ pushOp(true, slice.start, slice);
+ pushOp(false, slice.end, slice);
+ }
+ });
+ }
+ ops.sort(function(a, b) { return a.time - b.time; });
+
+ const rects = [];
+ /**
+ * Build a row of rects which display one way for unimportant activity,
+ * and during important slices, show up as those important slices.
+ *
+ * If an important slice starts in the middle of another,
+ * just drop it on the floor.
+ */
+ const genericColorId = ColorScheme.getColorIdForReservedName(
+ 'generic_work');
+ const pushRect = function(start, end, slice) {
+ rects.push(new tr.ui.tracks.Rect(
+ slice, /* modelItem: show selection state of slice if present */
+ slice ? slice.title : '', /* title */
+ slice ? slice.colorId : genericColorId, /* colorId */
+ start, /* start */
+ end - start /* duration */));
+ };
+ let depth = 0;
+ let currentSlice = undefined;
+ let lastStart = undefined;
+ ops.forEach(function(op) {
+ depth += op.isStart ? 1 : -1;
+
+ if (currentSlice) {
+ // simply find end of current important slice
+ if (!op.isStart && op.slice === currentSlice) {
+ // important slice has ended
+ pushRect(lastStart, op.time, currentSlice);
+ lastStart = depth >= 1 ? op.time : undefined;
+ currentSlice = undefined;
+ }
+ } else {
+ if (op.isStart) {
+ if (depth === 1) {
+ lastStart = op.time;
+ currentSlice = op.slice;
+ } else if (op.slice) {
+ // switch to slice
+ if (op.time !== lastStart) {
+ pushRect(lastStart, op.time, undefined);
+ lastStart = op.time;
+ }
+ currentSlice = op.slice;
+ }
+ } else {
+ if (depth === 0) {
+ pushRect(lastStart, op.time, undefined);
+ lastStart = undefined;
+ }
+ }
+ }
+ });
+ return rects;
+ };
+
+ ProcessSummaryTrack.prototype = {
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get process() {
+ return this.process_;
+ },
+
+ set process(process) {
+ this.process_ = process;
+ this.rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+ }
+ };
+
+ return {
+ ProcessSummaryTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html
new file mode 100644
index 00000000000..1d071f9d0ce
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_summary_track_test.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/model.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/ui/tracks/process_summary_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ProcessSummaryTrack = tr.ui.tracks.ProcessSummaryTrack;
+
+ test('buildRectSimple', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ process = model.getOrCreateProcess(1);
+ // XXXX
+ // XXXX
+ const thread1 = process.getOrCreateThread(1);
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 1, duration: 4}));
+ const thread2 = process.getOrCreateThread(2);
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 4, duration: 4}));
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(rects.length, 1);
+ const rect = rects[0];
+ assert.closeTo(rect.start, 1, 1e-5);
+ assert.closeTo(rect.end, 8, 1e-5);
+ });
+
+ test('buildRectComplex', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ process = model.getOrCreateProcess(1);
+ // XXXX X X XX
+ // XXXX XXX X
+ const thread1 = process.getOrCreateThread(1);
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 1, duration: 4}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 9, duration: 1}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 11, duration: 1}));
+ thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 13, duration: 2}));
+ const thread2 = process.getOrCreateThread(2);
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 4, duration: 4}));
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 9, duration: 3}));
+ thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {start: 16, duration: 1}));
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(4, rects.length);
+ assert.closeTo(rects[0].start, 1, 1e-5);
+ assert.closeTo(rects[0].end, 8, 1e-5);
+ assert.closeTo(rects[1].start, 9, 1e-5);
+ assert.closeTo(rects[1].end, 12, 1e-5);
+ assert.closeTo(rects[2].start, 13, 1e-5);
+ assert.closeTo(rects[2].end, 15, 1e-5);
+ assert.closeTo(rects[3].start, 16, 1e-5);
+ assert.closeTo(rects[3].end, 17, 1e-5);
+ });
+
+ test('buildRectImportantSlice', function() {
+ let process;
+ const model = tr.c.TestUtils.newModel(function(model) {
+ // [ unimportant ]
+ // [important]
+ const a = tr.c.TestUtils.newSliceEx(
+ {title: 'unimportant', start: 4, duration: 21});
+ const b = tr.c.TestUtils.newSliceEx(
+ {title: 'important', start: 9, duration: 11});
+ b.important = true;
+ process = model.getOrCreateProcess(1);
+ process.getOrCreateThread(1).sliceGroup.pushSlices([a, b]);
+
+ model.importantSlice = b;
+ });
+
+ const rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+ assert.strictEqual(3, rects.length);
+ assert.closeTo(rects[0].start, 4, 1e-5);
+ assert.closeTo(rects[0].end, 9, 1e-5);
+ assert.closeTo(rects[1].start, 9, 1e-5);
+ assert.closeTo(rects[1].end, 20, 1e-5);
+ assert.closeTo(rects[2].start, 20, 1e-5);
+ assert.closeTo(rects[2].end, 25, 1e-5);
+
+ // middle rect represents important slice, so colorId & title are preserved
+ assert.strictEqual(rects[1].title, model.importantSlice.title);
+ assert.strictEqual(rects[1].colorId, model.importantSlice.colorId);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html
new file mode 100644
index 00000000000..1be51cbf4cb
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/tracks/process_memory_dump_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_track_base.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ProcessTrackBase = tr.ui.tracks.ProcessTrackBase;
+
+ /**
+ * @constructor
+ */
+ const ProcessTrack = tr.ui.b.define('process-track', ProcessTrackBase);
+
+ ProcessTrack.prototype = {
+ __proto__: ProcessTrackBase.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ProcessTrackBase.prototype.decorate.call(this, viewport);
+ },
+
+ drawTrack(type) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.INSTANT_EVENT: {
+ if (!this.processBase.instantEvents ||
+ this.processBase.instantEvents.length === 0) {
+ break;
+ }
+
+ const ctx = this.context();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+
+ tr.ui.b.drawInstantSlicesAsLines(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.processBase.instantEvents,
+ 2);
+
+ ctx.restore();
+
+ break;
+ }
+
+ case tr.ui.tracks.DrawType.BACKGROUND:
+ this.drawBackground_();
+ // Don't bother recursing further, Process is the only level that
+ // draws backgrounds.
+ return;
+ }
+
+ tr.ui.tracks.ContainerTrack.prototype.drawTrack.call(this, type);
+ },
+
+ drawBackground_() {
+ const ctx = this.context();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ let draw = false;
+ ctx.fillStyle = '#eee';
+ for (let i = 0; i < this.children.length; ++i) {
+ if (!(this.children[i] instanceof tr.ui.tracks.Track) ||
+ (this.children[i] instanceof tr.ui.tracks.SpacingTrack)) {
+ continue;
+ }
+
+ draw = !draw;
+ if (!draw) continue;
+
+ const bounds = this.children[i].getBoundingClientRect();
+ ctx.fillRect(0, pixelRatio * (bounds.top - canvasBounds.top),
+ ctx.canvas.width, pixelRatio * bounds.height);
+ }
+ },
+
+ // Process maps to processBase because we derive from ProcessTrackBase.
+ set process(process) {
+ this.processBase = process;
+ },
+
+ get process() {
+ return this.processBase;
+ },
+
+ get eventContainer() {
+ return this.process;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ProcessTrackBase.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.process, this);
+ },
+
+ appendMemoryDumpTrack_() {
+ const processMemoryDumps = this.process.memoryDumps;
+ if (processMemoryDumps.length) {
+ const pmdt = new tr.ui.tracks.ProcessMemoryDumpTrack(this.viewport_);
+ pmdt.memoryDumps = processMemoryDumps;
+ Polymer.dom(this).appendChild(pmdt);
+ }
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onPickHit(instantEvent) {
+ selection.push(instantEvent);
+ }
+ const instantEventWidth = 2 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.processBase.instantEvents,
+ function(x) { return x.start; },
+ function(x) { return x.duration + instantEventWidth; },
+ loWX, hiWX,
+ onPickHit.bind(this));
+
+ tr.ui.tracks.ContainerTrack.prototype.
+ addIntersectingEventsInRangeToSelectionInWorldSpace.
+ apply(this, arguments);
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ this.addClosestInstantEventToSelection(this.processBase.instantEvents,
+ worldX, worldMaxDist, selection);
+ tr.ui.tracks.ContainerTrack.prototype.addClosestEventToSelection.
+ apply(this, arguments);
+ }
+ };
+
+ return {
+ ProcessTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css
new file mode 100644
index 00000000000..25fa5f015b7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.css
@@ -0,0 +1,39 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.process-track-header {
+ display: flex;
+ flex: 0 0 auto;
+ background-image: -webkit-gradient(linear,
+ 0 0, 100% 0,
+ from(#E5E5E5),
+ to(#D1D1D1));
+ border-bottom: 1px solid #8e8e8e;
+ border-top: 1px solid white;
+ font-size: 75%;
+}
+
+.process-track-name {
+ flex-grow: 1;
+}
+
+.process-track-name:before {
+ content: '\25B8'; /* Right triangle */
+ padding: 0 5px;
+}
+
+.process-track-base.expanded .process-track-name:before {
+ content: '\25BE'; /* Down triangle */
+}
+
+.process-track-close {
+ color: black;
+ border: 1px solid transparent;
+ padding: 0px 2px;
+}
+
+.process-track-close:hover {
+ border: 1px solid grey;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html
new file mode 100644
index 00000000000..89358b8411e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/process_track_base.html
@@ -0,0 +1,313 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/process_track_base.css">
+
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/model_settings.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/counter_track.html">
+<link rel="import" href="/tracing/ui/tracks/frame_track.html">
+<link rel="import" href="/tracing/ui/tracks/object_instance_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/other_threads_track.html">
+<link rel="import" href="/tracing/ui/tracks/process_summary_track.html">
+<link rel="import" href="/tracing/ui/tracks/spacing_track.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ const ObjectSnapshotView = tr.ui.analysis.ObjectSnapshotView;
+ const ObjectInstanceView = tr.ui.analysis.ObjectInstanceView;
+ const SpacingTrack = tr.ui.tracks.SpacingTrack;
+
+ /**
+ * Visualizes a Process by building ThreadTracks and CounterTracks.
+ * @constructor
+ */
+ const ProcessTrackBase =
+ tr.ui.b.define('process-track-base', tr.ui.tracks.ContainerTrack);
+
+ ProcessTrackBase.prototype = {
+
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+
+ this.processBase_ = undefined;
+
+ Polymer.dom(this).classList.add('process-track-base');
+ Polymer.dom(this).classList.add('expanded');
+
+ this.processNameEl_ = tr.ui.b.createSpan();
+ Polymer.dom(this.processNameEl_).classList.add('process-track-name');
+
+ this.closeEl_ = tr.ui.b.createSpan();
+ Polymer.dom(this.closeEl_).classList.add('process-track-close');
+ this.closeEl_.textContent = 'X';
+
+ this.headerEl_ = tr.ui.b.createDiv({className: 'process-track-header'});
+ Polymer.dom(this.headerEl_).appendChild(this.processNameEl_);
+ Polymer.dom(this.headerEl_).appendChild(this.closeEl_);
+ this.headerEl_.addEventListener('click', this.onHeaderClick_.bind(this));
+
+ Polymer.dom(this).appendChild(this.headerEl_);
+ },
+
+ get processBase() {
+ return this.processBase_;
+ },
+
+ set processBase(processBase) {
+ this.processBase_ = processBase;
+
+ if (this.processBase_) {
+ const modelSettings = new tr.model.ModelSettings(
+ this.processBase_.model);
+ const defaultValue = this.processBase_.important;
+ this.expanded = modelSettings.getSettingFor(
+ this.processBase_, 'expanded', defaultValue);
+ }
+
+ this.updateContents_();
+ },
+
+ get expanded() {
+ return Polymer.dom(this).classList.contains('expanded');
+ },
+
+ set expanded(expanded) {
+ expanded = !!expanded;
+
+ if (this.expanded === expanded) return;
+
+ Polymer.dom(this).classList.toggle('expanded');
+
+ // Expanding and collapsing tracks is, essentially, growing and shrinking
+ // the viewport. We dispatch a change event to trigger any processing
+ // to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ if (!this.processBase_) return;
+
+ const modelSettings = new tr.model.ModelSettings(this.processBase_.model);
+ modelSettings.setSettingFor(this.processBase_, 'expanded', expanded);
+ this.updateContents_();
+ this.viewport.rebuildEventToTrackMap();
+ this.viewport.rebuildContainerToTrackMap();
+ },
+
+ set visible(visible) {
+ if (visible === this.visible) return;
+ this.hidden = !visible;
+
+ tr.b.dispatchSimpleEvent(this, 'visibility');
+ // Changing the visibility of the tracks can grow and shrink the viewport.
+ // We dispatch a change event to trigger any processing to happen.
+ this.viewport_.dispatchChangeEvent();
+
+ if (!this.processBase_) return;
+
+ this.updateContents_();
+ this.viewport.rebuildEventToTrackMap();
+ this.viewport.rebuildContainerToTrackMap();
+ },
+
+ get visible() {
+ return !this.hidden;
+ },
+
+ get hasVisibleContent() {
+ if (this.expanded) {
+ return this.children.length > 1;
+ }
+ return true;
+ },
+
+ onHeaderClick_(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (e.target === this.closeEl_) {
+ this.visible = false;
+ } else {
+ this.expanded = !this.expanded;
+ }
+ },
+
+ updateContents_() {
+ this.clearTracks_();
+
+ if (!this.processBase_) return;
+
+ Polymer.dom(this.processNameEl_).textContent =
+ this.processBase_.userFriendlyName;
+ this.headerEl_.title = this.processBase_.userFriendlyDetails;
+
+ // Create the object instance tracks for this process.
+ this.willAppendTracks_();
+ if (this.expanded) {
+ this.appendMemoryDumpTrack_();
+ this.appendObjectInstanceTracks_();
+ this.appendCounterTracks_();
+ this.appendFrameTrack_();
+ this.appendThreadTracks_();
+ } else {
+ this.appendSummaryTrack_();
+ }
+ this.didAppendTracks_();
+ },
+
+ willAppendTracks_() {
+ },
+
+ didAppendTracks_() {
+ },
+
+ appendMemoryDumpTrack_() {
+ },
+
+ appendSummaryTrack_() {
+ const track = new tr.ui.tracks.ProcessSummaryTrack(this.viewport);
+ track.process = this.process;
+ if (!track.hasVisibleContent) return;
+ Polymer.dom(this).appendChild(track);
+ // no spacing track, since this track only shown in collapsed state
+ },
+
+ appendFrameTrack_() {
+ const frames = this.process ? this.process.frames : undefined;
+ if (!frames || !frames.length) return;
+
+ const track = new tr.ui.tracks.FrameTrack(this.viewport);
+ track.frames = frames;
+ Polymer.dom(this).appendChild(track);
+ },
+
+ appendObjectInstanceTracks_() {
+ const instancesByTypeName =
+ this.processBase_.objects.getAllInstancesByTypeName();
+ const instanceTypeNames = Object.keys(instancesByTypeName);
+ instanceTypeNames.sort();
+
+ let didAppendAtLeastOneTrack = false;
+ instanceTypeNames.forEach(function(typeName) {
+ const allInstances = instancesByTypeName[typeName];
+
+ // If a object snapshot has a view it will be shown,
+ // unless the view asked for it to not be shown.
+ let instanceViewInfo = ObjectInstanceView.getTypeInfo(
+ undefined, typeName);
+ let snapshotViewInfo = ObjectSnapshotView.getTypeInfo(
+ undefined, typeName);
+ if (instanceViewInfo && !instanceViewInfo.metadata.showInTrackView) {
+ instanceViewInfo = undefined;
+ }
+ if (snapshotViewInfo && !snapshotViewInfo.metadata.showInTrackView) {
+ snapshotViewInfo = undefined;
+ }
+ const hasViewInfo = instanceViewInfo || snapshotViewInfo;
+
+ // There are some instances that don't merit their own track in
+ // the UI. Filter them out.
+ const visibleInstances = [];
+ for (let i = 0; i < allInstances.length; i++) {
+ const instance = allInstances[i];
+
+ // Do not create tracks for instances that have no snapshots.
+ if (instance.snapshots.length === 0) continue;
+
+ // Do not create tracks for instances that have implicit snapshots
+ // and don't have a view.
+ if (instance.hasImplicitSnapshots && !hasViewInfo) continue;
+
+ visibleInstances.push(instance);
+ }
+ if (visibleInstances.length === 0) return;
+
+ // Look up the constructor for this track, or use the default
+ // constructor if none exists.
+ let trackConstructor =
+ tr.ui.tracks.ObjectInstanceTrack.getConstructor(
+ undefined, typeName);
+ if (!trackConstructor) {
+ snapshotViewInfo = ObjectSnapshotView.getTypeInfo(
+ undefined, typeName);
+ if (snapshotViewInfo && snapshotViewInfo.metadata.showInstances) {
+ trackConstructor = tr.ui.tracks.ObjectInstanceGroupTrack;
+ } else {
+ trackConstructor = tr.ui.tracks.ObjectInstanceTrack;
+ }
+ }
+ const track = new trackConstructor(this.viewport);
+ track.objectInstances = visibleInstances;
+ Polymer.dom(this).appendChild(track);
+ didAppendAtLeastOneTrack = true;
+ }, this);
+ if (didAppendAtLeastOneTrack) {
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }
+ },
+
+ appendCounterTracks_() {
+ // Add counter tracks for this process.
+ const counters = Object.values(this.processBase.counters);
+ counters.sort(tr.model.Counter.compare);
+
+ // Create the counters for this process.
+ counters.forEach(function(counter) {
+ const track = new tr.ui.tracks.CounterTrack(this.viewport);
+ track.counter = counter;
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ }.bind(this));
+ },
+
+ appendThreadTracks_() {
+ // Get a sorted list of threads.
+ const threads = Object.values(this.processBase.threads);
+ threads.sort(tr.model.Thread.compare);
+
+ // Create the threads.
+ const otherThreads = [];
+ let hasVisibleThreads = false;
+ threads.forEach(function(thread) {
+ const track = new tr.ui.tracks.ThreadTrack(this.viewport);
+ track.thread = thread;
+ if (!track.hasVisibleContent) return;
+
+ if (track.hasSlices) {
+ hasVisibleThreads = true;
+ Polymer.dom(this).appendChild(track);
+ Polymer.dom(this).appendChild(new SpacingTrack(this.viewport));
+ } else if (track.hasTimeSlices) {
+ otherThreads.push(thread);
+ }
+ }.bind(this));
+
+ if (otherThreads.length > 0) {
+ // If there's only 1 thread with scheduling-only information don't
+ // bother making a group, just display it directly
+ // Similarly if we are a process with only scheduling-only threads
+ // don't bother making a group as the process itself serves
+ // as the collapsable group
+ const track = new tr.ui.tracks.OtherThreadsTrack(this.viewport);
+ track.threads = otherThreads;
+ track.collapsible = otherThreads.length > 1 && hasVisibleThreads;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+ };
+
+ return {
+ ProcessTrackBase,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css
new file mode 100644
index 00000000000..0467c91562c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.css
@@ -0,0 +1,8 @@
+/* Copyright (c) 2014 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.rect-track {
+ height: 18px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html
new file mode 100644
index 00000000000..65e073d32ee
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track.html
@@ -0,0 +1,249 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/rect_track.css">
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/model/proxy_selectable_item.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/fast_rect_renderer.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Rect objects.
+ * @constructor
+ * @extends {Track}
+ */
+ const RectTrack = tr.ui.b.define(
+ 'rect-track', tr.ui.tracks.Track);
+
+ RectTrack.prototype = {
+
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('rect-track');
+ this.asyncStyle_ = false;
+ this.rects_ = null;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ set selectionGenerator(generator) {
+ this.heading_.selectionGenerator = generator;
+ },
+
+ set expanded(expanded) {
+ this.heading_.expanded = !!expanded;
+ },
+
+ set arrowVisible(arrowVisible) {
+ this.heading_.arrowVisible = !!arrowVisible;
+ },
+
+ get expanded() {
+ return this.heading_.expanded;
+ },
+
+ get asyncStyle() {
+ return this.asyncStyle_;
+ },
+
+ set asyncStyle(v) {
+ this.asyncStyle_ = !!v;
+ },
+
+ get rects() {
+ return this.rects_;
+ },
+
+ set rects(rects) {
+ this.rects_ = rects || [];
+ this.invalidateDrawingContainer();
+ },
+
+ get height() {
+ return window.getComputedStyle(this).height;
+ },
+
+ set height(height) {
+ this.style.height = height;
+ this.invalidateDrawingContainer();
+ },
+
+ get hasVisibleContent() {
+ return this.rects_.length > 0;
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GENERAL_EVENT:
+ this.drawRects_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawRects_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+
+ ctx.save();
+ const bounds = this.getBoundingClientRect();
+ tr.ui.b.drawSlices(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ bounds.height,
+ this.rects_,
+ this.asyncStyle_);
+ ctx.restore();
+
+ if (bounds.height <= 6) return;
+
+ let fontSize;
+ let yOffset;
+ if (bounds.height < 15) {
+ fontSize = 6;
+ yOffset = 1.0;
+ } else {
+ fontSize = 10;
+ yOffset = 2.5;
+ }
+ tr.ui.b.drawLabels(
+ ctx,
+ this.viewport.currentDisplayTransform,
+ viewLWorld,
+ viewRWorld,
+ this.rects_,
+ this.asyncStyle_,
+ fontSize,
+ yOffset);
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ if (this.rects_ === undefined || this.rects_ === null) {
+ return;
+ }
+
+ this.rects_.forEach(function(rect) {
+ rect.addToTrackMap(eventToTrackMap, this);
+ }, this);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onRect(rect) {
+ rect.addToSelection(selection);
+ }
+ onRect = onRect.bind(this);
+ const instantEventWidth = 2 * viewPixWidthWorld;
+ tr.b.iterateOverIntersectingIntervals(this.rects_,
+ function(x) { return x.start; },
+ function(x) {
+ return x.duration === 0 ?
+ x.duration + instantEventWidth :
+ x.duration;
+ },
+ loWX, hiWX,
+ onRect);
+ },
+
+ /**
+ * Add the item to the left or right of the provided event, if any, to the
+ * selection.
+ * @param {rect} The current rect.
+ * @param {Number} offset Number of rects away from the event to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ const index = this.rects_.findIndex(rect => rect.modelItem === event);
+ if (index === -1) return false;
+
+ const newIndex = index + offset;
+ if (newIndex < 0 || newIndex >= this.rects_.length) return false;
+
+ this.rects_[newIndex].addToSelection(selection);
+ return true;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ for (let i = 0; i < this.rects_.length; ++i) {
+ // TODO(petrcermak): Rather than unpacking the proxy item here,
+ // we should probably add an addToSelectionIfMatching(selection, filter)
+ // method to SelectableItem (#900).
+ const modelItem = this.rects_[i].modelItem;
+ if (!modelItem) continue;
+
+ if (filter.matchSlice(modelItem)) {
+ selection.push(modelItem);
+ }
+ }
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const rect = tr.b.findClosestIntervalInSortedIntervals(
+ this.rects_,
+ function(x) { return x.start; },
+ function(x) { return x.end; },
+ worldX,
+ worldMaxDist);
+
+ if (!rect) return;
+
+ rect.addToSelection(selection);
+ }
+ };
+
+ /**
+ * A filled rectangle with a title.
+ *
+ * @constructor
+ * @extends {ProxySelectableItem}
+ */
+ function Rect(modelItem, title, colorId, start, duration) {
+ tr.model.ProxySelectableItem.call(this, modelItem);
+ this.title = title;
+ this.colorId = colorId;
+ this.start = start;
+ this.duration = duration;
+ this.end = start + duration;
+ }
+
+ Rect.prototype = {
+ __proto__: tr.model.ProxySelectableItem.prototype
+ };
+
+ return {
+ RectTrack,
+ Rect,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html
new file mode 100644
index 00000000000..ec81a8835d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/rect_track_test.html
@@ -0,0 +1,412 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/slice.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const EventSet = tr.model.EventSet;
+ const RectTrack = tr.ui.tracks.RectTrack;
+ const Rect = tr.ui.tracks.Rect;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const Viewport = tr.ui.TimelineViewport;
+
+ test('instantiate_withRects', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasicRects';
+ track.rects = [
+ new Rect(undefined, 'a', 0, 1, 1),
+ new Rect(undefined, 'b', 1, 2.1, 4.8),
+ new Rect(undefined, 'b', 1, 7, 0.5),
+ new Rect(undefined, 'c', 2, 7.6, 0.4)
+ ];
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 8.8, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_withSlices', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testBasicSlices';
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 8.8, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_shrinkingRectSize', function() {
+ const div = document.createElement('div');
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.heading = 'testShrinkingRectSizes';
+ let x = 0;
+ const widths = [10, 5, 4, 3, 2, 1, 0.5, 0.4, 0.3, 0.2, 0.1, 0.05];
+ const slices = [];
+ for (let i = 0; i < widths.length; i++) {
+ const s = new Rect(undefined, 'a', 1, x, widths[i]);
+ x += s.duration + 0.5;
+ slices.push(s);
+ }
+ track.rects = slices;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 1.1 * x, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_elide', function() {
+ const optDicts = [{ trackName: 'elideOff', elide: false },
+ { trackName: 'elideOn', elide: true }];
+
+ const tooLongTitle = 'Unless eliding this SHOULD NOT BE DISPLAYED. ';
+ const bigTitle = 'Very big title name that goes on longer ' +
+ 'than you may expect';
+
+ for (const dictIndex in optDicts) {
+ const dict = optDicts[dictIndex];
+
+ const div = document.createElement('div');
+ Polymer.dom(div).appendChild(document.createTextNode(dict.trackName));
+
+ const viewport = new Viewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+
+ this.addHTMLOutput(div);
+ drawingContainer.invalidate();
+
+ track.SHOULD_ELIDE_TEXT = dict.elide;
+ track.heading = 'Visual: ' + dict.trackName;
+ track.rects = [
+ // title, colorId, start, args, opt_duration
+ new Rect(undefined, 'a ' + tooLongTitle + bigTitle, 0, 1, 1),
+ new Rect(undefined, bigTitle, 1, 2.1, 4.8),
+ new Rect(undefined, 'cccc cccc cccc', 1, 7, 0.5),
+ new Rect(undefined, 'd', 2, 7.6, 1.0)
+ ];
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 9.5, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+ }
+ });
+
+ test('findAllObjectsMatchingInRectTrack', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+ const selection = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('b'), selection);
+
+ const predictedSelection = new EventSet(
+ [track.rects[1].modelItem, track.rects[2].modelItem]);
+ assert.isTrue(selection.equals(predictedSelection));
+ });
+
+ test('selectionHitTesting', function() {
+ const testEl = document.createElement('div');
+ Polymer.dom(testEl).appendChild(
+ tr.ui.b.createScopedStyle('heading { width: 100px; }'));
+ testEl.style.width = '600px';
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ track.heading = 'testSelectionHitTesting';
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 5, {}, 4.8)
+ ];
+ const y = track.getBoundingClientRect().top + 5;
+ const pixelRatio = window.devicePixelRatio || 1;
+ const wW = 10;
+ const vW = drawingContainer.canvas.getBoundingClientRect().width;
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, wW, vW * pixelRatio);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let selection = new EventSet();
+ let x = (1.5 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(new EventSet(track.rects[0].modelItem)));
+
+ selection = new EventSet();
+ x = (2.1 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.strictEqual(0, selection.length);
+
+ selection = new EventSet();
+ x = (6.8 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(new EventSet(track.rects[1].modelItem)));
+
+ selection = new EventSet();
+ x = (9.9 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.strictEqual(0, selection.length);
+ });
+
+ test('elide', function() {
+ const testEl = document.createElement('div');
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new RectTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(testEl);
+
+ drawingContainer.updateCanvasSizeIfNeeded_();
+
+ const bigtitle = 'Super duper long long title ' +
+ 'holy moly when did you get so verbose?';
+ const smalltitle = 'small';
+ track.heading = 'testElide';
+ track.rects = [
+ // title, colorId, start, args, opt_duration
+ new ThreadSlice('', bigtitle, 0, 1, {}, 1),
+ new ThreadSlice('', smalltitle, 1, 2, {}, 1)
+ ];
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, 3.3, track.clientWidth);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let stringWidthPair = undefined;
+ const pixWidth = dt.xViewVectorToWorld(1);
+
+ // Small titles on big slices are not elided.
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ smalltitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ smalltitle),
+ 1);
+ assert.strictEqual(smalltitle, stringWidthPair.string);
+
+ // Keep shrinking the slice until eliding starts.
+ let elidedWhenSmallEnough = false;
+ for (let sliceLength = 1; sliceLength >= 0.00001; sliceLength /= 2.0) {
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ smalltitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ smalltitle),
+ sliceLength);
+ if (stringWidthPair.string.length < smalltitle.length) {
+ elidedWhenSmallEnough = true;
+ break;
+ }
+ }
+ assert.isTrue(elidedWhenSmallEnough);
+
+ // Big titles are elided immediately.
+ let superBigTitle = '';
+ for (let x = 0; x < 10; x++) {
+ superBigTitle += bigtitle;
+ }
+ stringWidthPair =
+ tr.ui.b.elidedTitleCache_.get(
+ track.context(),
+ pixWidth,
+ superBigTitle,
+ tr.ui.b.elidedTitleCache_.labelWidth(
+ track.context(),
+ superBigTitle),
+ 1);
+ assert.isTrue(stringWidthPair.string.length < superBigTitle.length);
+
+ // And elided text ends with ...
+ const len = stringWidthPair.string.length;
+ assert.strictEqual('...', stringWidthPair.string.substring(len - 3, len));
+ });
+
+ test('rectTrackAddItemNearToProvidedEvent', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+ let sel = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('b'), sel);
+
+ // Select to the right of B.
+ const selRight = new EventSet();
+ let ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), 1, selRight);
+ assert.isTrue(ret);
+ assert.strictEqual(
+ track.rects[2].modelItem, tr.b.getFirstElement(selRight));
+
+ // Select to the right of the 2nd b.
+ const selRight2 = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), 2, selRight2);
+ assert.isTrue(ret);
+ assert.strictEqual(
+ track.rects[3].modelItem, tr.b.getFirstElement(selRight2));
+
+ // Select to 2 to the right of the 2nd b.
+ const selRightOfRight = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(selRight), 1, selRightOfRight);
+ assert.isTrue(ret);
+ assert.strictEqual(track.rects[3].modelItem,
+ tr.b.getFirstElement(selRightOfRight));
+
+ // Select to the right of the rightmost slice.
+ let selNone = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(selRightOfRight), 1, selNone);
+ assert.isFalse(ret);
+ assert.strictEqual(0, selNone.length);
+
+ // Select A and then select left.
+ sel = new EventSet();
+ track.addAllEventsMatchingFilterToSelection(
+ new tr.c.TitleOrCategoryFilter('a'), sel);
+
+ selNone = new EventSet();
+ ret = track.addEventNearToProvidedEventToSelection(
+ tr.b.getFirstElement(sel), -1, selNone);
+ assert.isFalse(ret);
+ assert.strictEqual(0, selNone.length);
+ });
+
+ test('rectTrackAddClosestEventToSelection', function() {
+ const track = new RectTrack(new tr.ui.TimelineViewport());
+ track.rects = [
+ new ThreadSlice('', 'a', 0, 1, {}, 1),
+ new ThreadSlice('', 'b', 1, 2.1, {}, 4.8),
+ new ThreadSlice('', 'b', 1, 7, {}, 0.5),
+ new ThreadSlice('', 'c', 2, 7.6, {}, 0.4)
+ ];
+
+ // Before with not range.
+ let sel = new EventSet();
+ track.addClosestEventToSelection(0, 0, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Before with negative range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.5, -10, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Before first slice.
+ sel = new EventSet();
+ track.addClosestEventToSelection(0.5, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[0].modelItem)));
+
+ // Within first slice closer to start.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.3, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[0].modelItem)));
+
+ // Between slices with good range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(2.08, 3, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[1].modelItem)));
+
+ // Between slices with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(2.05, 0.03, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // Within slice closer to end.
+ sel = new EventSet();
+ track.addClosestEventToSelection(6, 100, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[1].modelItem)));
+
+ // Within slice with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(1.8, 0.1, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+
+ // After last slice with good range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(8.5, 1, 0, 0, sel);
+ assert.isTrue(sel.equals(new EventSet(track.rects[3].modelItem)));
+
+ // After last slice with bad range.
+ sel = new EventSet();
+ track.addClosestEventToSelection(10, 1, 0, 0, sel);
+ assert.strictEqual(0, sel.length);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html
new file mode 100644
index 00000000000..1f764019cfc
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Sample objects.
+ * @constructor
+ * @extends {RectTrack}
+ */
+ const SampleTrack = tr.ui.b.define(
+ 'sample-track', tr.ui.tracks.RectTrack);
+
+ SampleTrack.prototype = {
+
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get samples() {
+ return this.rects;
+ },
+
+ set samples(samples) {
+ this.rects = samples;
+ }
+ };
+
+ return {
+ SampleTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html
new file mode 100644
index 00000000000..0fb17df65f1
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/sample_track_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/profile_node.html">
+<link rel="import" href="/tracing/model/sample.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/sample_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SampleTrack = tr.ui.tracks.SampleTrack;
+ const Sample = tr.model.Sample;
+ const ProfileNode = tr.model.ProfileNode;
+
+ test('modelMapping', function() {
+ const track = new SampleTrack(new tr.ui.TimelineViewport());
+ const node = new ProfileNode(1, {
+ functionName: 'a'
+ }, undefined);
+ const sample = new Sample(10, 'instructions_retired', node);
+ track.samples = [sample];
+ const me0 = track.rects[0].modelItem;
+ assert.strictEqual(me0, sample);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html
new file mode 100644
index 00000000000..36f09566b07
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/multi_row_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays a SliceGroup.
+ * @constructor
+ * @extends {MultiRowTrack}
+ */
+ const SliceGroupTrack = tr.ui.b.define(
+ 'slice-group-track', tr.ui.tracks.MultiRowTrack);
+
+ SliceGroupTrack.prototype = {
+
+ __proto__: tr.ui.tracks.MultiRowTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('slice-group-track');
+ this.group_ = undefined;
+ // Set the collapse threshold so we don't collapse by default, but the
+ // user can explicitly collapse if they want it.
+ this.defaultToCollapsedWhenSubRowCountMoreThan = 100;
+ },
+
+ addSubTrack_(slices) {
+ const track = new tr.ui.tracks.SliceTrack(this.viewport);
+ track.slices = slices;
+ Polymer.dom(this).appendChild(track);
+ return track;
+ },
+
+ get group() {
+ return this.group_;
+ },
+
+ set group(group) {
+ this.group_ = group;
+ this.setItemsToGroup(this.group_.slices, this.group_);
+ },
+
+ get eventContainer() {
+ return this.group;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.MultiRowTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.group, this);
+ },
+
+ /**
+ * Breaks up the list of slices into N rows, each of which is a list of
+ * slices that are non overlapping.
+ */
+ buildSubRows_(slices) {
+ const precisionUnit = this.group.model.intrinsicTimeUnit;
+
+ // This function works by walking through slices by start time.
+ //
+ // The basic idea here is to insert each slice as deep into the subrow
+ // list as it can go such that every subSlice is fully contained by its
+ // parent slice.
+ //
+ // Visually, if we start with this:
+ // 0: [ a ]
+ // 1: [ b ]
+ // 2: [c][d]
+ //
+ // To place this slice:
+ // [e]
+ // We first check row 2's last item, [d]. [e] wont fit into [d] (they dont
+ // even intersect). So we go to row 1. That gives us [b], and [d] wont fit
+ // into that either. So, we go to row 0 and its last slice, [a]. That can
+ // completely contain [e], so that means we should add [e] as a subchild
+ // of [a]. That puts it on row 1, yielding:
+ // 0: [ a ]
+ // 1: [ b ][e]
+ // 2: [c][d]
+ //
+ // If we then get this slice:
+ // [f]
+ // We do the same deepest-to-shallowest walk of the subrows trying to fit
+ // it. This time, it doesn't fit in any open slice. So, we simply append
+ // it to row 0:
+ // 0: [ a ] [f]
+ // 1: [ b ][e]
+ // 2: [c][d]
+ if (!slices.length) return [];
+
+ const ops = [];
+ for (let i = 0; i < slices.length; i++) {
+ if (slices[i].subSlices) {
+ slices[i].subSlices.splice(0,
+ slices[i].subSlices.length);
+ }
+ ops.push(i);
+ }
+
+ ops.sort(function(ix, iy) {
+ const x = slices[ix];
+ const y = slices[iy];
+ if (x.start !== y.start) return x.start - y.start;
+
+ // Elements get inserted into the slices array in order of when the
+ // slices start. Because slices must be properly nested, we break
+ // start-time ties by assuming that the elements appearing earlier in
+ // the slices array (and thus ending earlier) start earlier.
+ return ix - iy;
+ });
+
+ const subRows = [[]];
+ this.badSlices_ = []; // TODO(simonjam): Connect this again.
+
+ for (let i = 0; i < ops.length; i++) {
+ const op = ops[i];
+ const slice = slices[op];
+
+ // Try to fit the slice into the existing subrows.
+ let inserted = false;
+ for (let j = subRows.length - 1; j >= 0; j--) {
+ if (subRows[j].length === 0) continue;
+
+ const insertedSlice = subRows[j][subRows[j].length - 1];
+ if (slice.start < insertedSlice.start) {
+ this.badSlices_.push(slice);
+ inserted = true;
+ }
+ if (insertedSlice.bounds(slice, precisionUnit)) {
+ // Insert it into subRow j + 1.
+ while (subRows.length <= j + 1) {
+ subRows.push([]);
+ }
+ subRows[j + 1].push(slice);
+ if (insertedSlice.subSlices) {
+ insertedSlice.subSlices.push(slice);
+ }
+ inserted = true;
+ break;
+ }
+ }
+ if (inserted) continue;
+
+ // Append it to subRow[0] as a root.
+ subRows[0].push(slice);
+ }
+
+ return subRows;
+ }
+ };
+
+ return {
+ SliceGroupTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html
new file mode 100644
index 00000000000..a8b5842f945
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_group_track_test.html
@@ -0,0 +1,299 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/slice_group.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ProcessTrack = tr.ui.tracks.ProcessTrack;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const SliceGroup = tr.model.SliceGroup;
+ const SliceGroupTrack = tr.ui.tracks.SliceGroupTrack;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('subRowBuilderBasic', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2}));
+ const sB = group.pushSlice(newSliceEx({title: 'a', start: 3, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderBasic2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderNestedExactly', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 4}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sB]);
+ assert.deepEqual(subRows[1], [sA]);
+ });
+
+ test('subRowBuilderInstantEvents', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderTwoInstantEvents', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0}));
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOutOfOrderAddition', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ][ b ]
+ // Where insertion is done backward.
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 1);
+ assert.strictEqual(subRows[0].length, 2);
+ assert.deepEqual(subRows[0], [sA, sB]);
+ });
+
+ test('subRowBuilderOutOfOrderAddition2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b ]
+ // Where insertion is done backward.
+ const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1}));
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 5}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.strictEqual(subRows[0].length, 1);
+ assert.strictEqual(subRows[1].length, 1);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOnNestedZeroLength', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ]
+ // [ b1 ] []<- b2 where b2.duration = 0 and b2.end === a.end.
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB1 = group.pushSlice(newSliceEx(
+ {title: 'b1', start: 1, duration: 2}));
+ const sB2 = group.pushSlice(newSliceEx(
+ {title: 'b2', start: 4, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA]);
+ assert.deepEqual(subRows[1], [sB1, sB2]);
+ });
+
+ test('subRowBuilderOnGroup1', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ] [ c ]
+ // [ b ]
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+ const sC = group.pushSlice(newSliceEx({title: 'c', start: 5, duration: 0}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+ const subRows = track.subRows;
+
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 2);
+ assert.deepEqual(subRows[0], [sA, sC]);
+ assert.deepEqual(subRows[1], [sB]);
+ });
+
+ test('subRowBuilderOnGroup2', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ // Pattern being tested:
+ // [ a ] [ d ]
+ // [ b ]
+ // [ c ]
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+ const sC = group.pushSlice(newSliceEx(
+ {title: 'c', start: 1.75, duration: 0.5}));
+ const sD = group.pushSlice(newSliceEx(
+ {title: 'c', start: 5, duration: 0.25}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+
+ const subRows = track.subRows;
+ assert.strictEqual(track.badSlices_.length, 0);
+ assert.strictEqual(subRows.length, 3);
+ assert.deepEqual(subRows[0], [sA, sD]);
+ assert.deepEqual(subRows[1], [sB]);
+ assert.deepEqual(subRows[2], [sC]);
+ });
+
+ test('trackFiltering', function() {
+ const m = new tr.Model();
+ const t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+ const group = t1.sliceGroup;
+
+ const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3}));
+ const sB = group.pushSlice(newSliceEx(
+ {title: 'b', start: 1.5, duration: 1}));
+
+ const track = new SliceGroupTrack(new tr.ui.TimelineViewport());
+ track.group = group;
+
+ assert.strictEqual(track.subRows.length, 2);
+ assert.isTrue(track.hasVisibleContent);
+ });
+
+ test('sliceGroupContainerMap', function() {
+ const vp = new tr.ui.TimelineViewport();
+ const containerToTrack = vp.containerToTrackMap;
+ const model = new tr.Model();
+ const process = model.getOrCreateProcess(123);
+ const thread = process.getOrCreateThread(456);
+ const group = new SliceGroup(thread);
+
+ const processTrack = new ProcessTrack(vp);
+ const threadTrack = new ThreadTrack(vp);
+ const groupTrack = new SliceGroupTrack(vp);
+ processTrack.process = process;
+ threadTrack.thread = thread;
+ groupTrack.group = group;
+ Polymer.dom(processTrack).appendChild(threadTrack);
+ Polymer.dom(threadTrack).appendChild(groupTrack);
+
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+
+ assert.isUndefined(containerToTrack.getTrackByStableId('123'));
+ assert.isUndefined(containerToTrack.getTrackByStableId('123.456'));
+ assert.isUndefined(
+ containerToTrack.getTrackByStableId('123.456.SliceGroup'));
+
+ vp.modelTrackContainer = {
+ addContainersToTrackMap(containerToTrackMap) {
+ processTrack.addContainersToTrackMap(containerToTrackMap);
+ },
+ addEventListener() {}
+ };
+ vp.rebuildContainerToTrackMap();
+
+ // Check that all tracks call childs' addContainersToTrackMap()
+ // by checking the resulting map.
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123'), processTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456'), threadTrack);
+ assert.strictEqual(
+ containerToTrack.getTrackByStableId('123.456.SliceGroup'), groupTrack);
+
+ // Check the track's eventContainer getter.
+ assert.strictEqual(processTrack.eventContainer, process);
+ assert.strictEqual(threadTrack.eventContainer, thread);
+ assert.strictEqual(groupTrack.eventContainer, group);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html
new file mode 100644
index 00000000000..1e1386bff66
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays an array of Slice objects.
+ * @constructor
+ * @extends {RectTrack}
+ */
+ const SliceTrack = tr.ui.b.define(
+ 'slice-track', tr.ui.tracks.RectTrack);
+
+ SliceTrack.prototype = {
+
+ __proto__: tr.ui.tracks.RectTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.RectTrack.prototype.decorate.call(this, viewport);
+ },
+
+ get slices() {
+ return this.rects;
+ },
+
+ set slices(slices) {
+ this.rects = slices;
+ }
+ };
+
+ return {
+ SliceTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html
new file mode 100644
index 00000000000..7ba42d3dc79
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/slice_track_test.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/slice.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const SliceTrack = tr.ui.tracks.SliceTrack;
+ const ThreadSlice = tr.model.ThreadSlice;
+
+ test('modelMapping', function() {
+ const track = new SliceTrack(new tr.ui.TimelineViewport());
+ const slice = new ThreadSlice('', 'a', 0, 1, {}, 1);
+ track.slices = [slice];
+ const me0 = track.rects[0].modelItem;
+ assert.strictEqual(slice, me0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css
new file mode 100644
index 00000000000..094eee0862d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.css
@@ -0,0 +1,7 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+.spacing-track {
+ height: 4px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html
new file mode 100644
index 00000000000..a321066daa2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/spacing_track.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/spacing_track.css">
+
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track used to provide whitespace between the tracks above and below it.
+ *
+ * @constructor
+ * @extends {tr.ui.tracks.Track}
+ */
+ const SpacingTrack = tr.ui.b.define('spacing-track', tr.ui.tracks.Track);
+
+ SpacingTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('spacing-track');
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ }
+ };
+
+ return {
+ SpacingTrack,
+ };
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html
new file mode 100644
index 00000000000..7a292c04113
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/stacked_bars_track.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays traces as stacked bars.
+ * @constructor
+ * @extends {Track}
+ */
+ const StackedBarsTrack = tr.ui.b.define(
+ 'stacked-bars-track', tr.ui.tracks.Track);
+
+ StackedBarsTrack.prototype = {
+
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('stacked-bars-track');
+ this.objectInstance_ = null;
+
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ Polymer.dom(this).appendChild(this.heading_);
+ },
+
+ set heading(heading) {
+ this.heading_.heading = heading;
+ },
+
+ get heading() {
+ return this.heading_.heading;
+ },
+
+ set tooltip(tooltip) {
+ this.heading_.tooltip = tooltip;
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ const objectSnapshots = this.objectInstance_.snapshots;
+ objectSnapshots.forEach(function(obj) {
+ eventToTrackMap.addEvent(obj, this);
+ }, this);
+ },
+
+ /**
+ * Used to hit-test clicks in the graph.
+ */
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ function onSnapshot(snapshot) {
+ selection.push(snapshot);
+ }
+
+ const snapshots = this.objectInstance_.snapshots;
+ const maxBounds = this.objectInstance_.parent.model.bounds.max;
+
+ tr.b.iterateOverIntersectingIntervals(
+ snapshots,
+ function(x) { return x.ts; },
+ function(x, i) {
+ if (i === snapshots.length - 1) {
+ if (snapshots.length === 1) {
+ return maxBounds;
+ }
+
+ return snapshots[i].ts - snapshots[i - 1].ts;
+ }
+
+ return snapshots[i + 1].ts - snapshots[i].ts;
+ },
+ loWX, hiWX,
+ onSnapshot);
+ },
+
+ /**
+ * Add the item to the left or right of the provided item, if any, to the
+ * selection.
+ * @param {slice} The current slice.
+ * @param {Number} offset Number of slices away from the object to look.
+ * @param {Selection} selection The selection to add an event to,
+ * if found.
+ * @return {boolean} Whether an event was found.
+ * @private
+ */
+ addEventNearToProvidedEventToSelection(event, offset, selection) {
+ if (!(event instanceof tr.model.ObjectSnapshot)) {
+ throw new Error('Unrecognized event');
+ }
+ const objectSnapshots = this.objectInstance_.snapshots;
+ const index = objectSnapshots.indexOf(event);
+ const newIndex = index + offset;
+ if (newIndex >= 0 && newIndex < objectSnapshots.length) {
+ selection.push(objectSnapshots[newIndex]);
+ return true;
+ }
+ return false;
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ },
+
+ addClosestEventToSelection(worldX, worldMaxDist, loY, hiY,
+ selection) {
+ const snapshot = tr.b.findClosestElementInSortedArray(
+ this.objectInstance_.snapshots,
+ function(x) { return x.ts; },
+ worldX,
+ worldMaxDist);
+
+ if (!snapshot) return;
+
+ selection.push(snapshot);
+ }
+ };
+
+ return {
+ StackedBarsTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css
new file mode 100644
index 00000000000..4e063bbad48
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.css
@@ -0,0 +1,10 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.thread-track {
+ flex-direction: column;
+ display: flex;
+ position: relative;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html
new file mode 100644
index 00000000000..c6ea8fa576c
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/tracing/ui/tracks/thread_track.css">
+
+<link rel="import" href="/tracing/base/utils.html">
+<link rel="import" href="/tracing/core/filter.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/async_slice_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/container_track.html">
+<link rel="import" href="/tracing/ui/tracks/sample_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_group_track.html">
+<link rel="import" href="/tracing/ui/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * Visualizes a Thread using a series of SliceTracks.
+ * @constructor
+ */
+ const ThreadTrack = tr.ui.b.define('thread-track',
+ tr.ui.tracks.ContainerTrack);
+ ThreadTrack.prototype = {
+ __proto__: tr.ui.tracks.ContainerTrack.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('thread-track');
+ this.heading_ = document.createElement('tr-ui-b-heading');
+ },
+
+ get thread() {
+ return this.thread_;
+ },
+
+ set thread(thread) {
+ this.thread_ = thread;
+ this.updateContents_();
+ },
+
+ get hasVisibleContent() {
+ return this.tracks_.length > 0;
+ },
+
+ get hasSlices() {
+ return this.thread_.asyncSliceGroup.length > 0 ||
+ this.thread_.sliceGroup.length > 0;
+ },
+
+ get hasTimeSlices() {
+ return this.thread_.timeSlices;
+ },
+
+ get eventContainer() {
+ return this.thread;
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ tr.ui.tracks.ContainerTrack.prototype.addContainersToTrackMap.apply(
+ this, arguments);
+ containerToTrackMap.addContainer(this.thread, this);
+ },
+
+ updateContents_() {
+ this.detach();
+
+ if (!this.thread_) return;
+
+ this.heading_.heading = this.thread_.userFriendlyName;
+ this.heading_.tooltip = this.thread_.userFriendlyDetails;
+
+ if (this.thread_.asyncSliceGroup.length) {
+ this.appendAsyncSliceTracks_();
+ }
+
+ this.appendThreadSamplesTracks_();
+
+ let needsHeading = false;
+ if (this.thread_.timeSlices) {
+ const timeSlicesTrack = new tr.ui.tracks.SliceTrack(this.viewport);
+ timeSlicesTrack.heading = '';
+ timeSlicesTrack.height = tr.ui.b.THIN_SLICE_HEIGHT + 'px';
+ timeSlicesTrack.slices = this.thread_.timeSlices;
+ if (timeSlicesTrack.hasVisibleContent) {
+ needsHeading = true;
+ Polymer.dom(this).appendChild(timeSlicesTrack);
+ }
+ }
+
+ if (this.thread_.sliceGroup.length) {
+ const track = new tr.ui.tracks.SliceGroupTrack(this.viewport);
+ track.heading = this.thread_.userFriendlyName;
+ track.tooltip = this.thread_.userFriendlyDetails;
+ track.group = this.thread_.sliceGroup;
+ if (track.hasVisibleContent) {
+ needsHeading = false;
+ Polymer.dom(this).appendChild(track);
+ }
+ }
+
+ if (needsHeading) {
+ Polymer.dom(this).appendChild(this.heading_);
+ }
+ },
+
+ appendAsyncSliceTracks_() {
+ const subGroups = this.thread_.asyncSliceGroup.viewSubGroups;
+ // TODO(kraynov): Support nested sub-groups.
+ subGroups.forEach(function(subGroup) {
+ const asyncTrack = new tr.ui.tracks.AsyncSliceGroupTrack(this.viewport);
+ asyncTrack.group = subGroup;
+ asyncTrack.heading = subGroup.title;
+ if (asyncTrack.hasVisibleContent) {
+ Polymer.dom(this).appendChild(asyncTrack);
+ }
+ }, this);
+ },
+
+ appendThreadSamplesTracks_() {
+ const threadSamples = this.thread_.samples;
+ if (threadSamples === undefined || threadSamples.length === 0) {
+ return;
+ }
+ const samplesByTitle = {};
+ threadSamples.forEach(function(sample) {
+ if (samplesByTitle[sample.title] === undefined) {
+ samplesByTitle[sample.title] = [];
+ }
+ samplesByTitle[sample.title].push(sample);
+ });
+
+ const sampleTitles = Object.keys(samplesByTitle);
+ sampleTitles.sort();
+
+ sampleTitles.forEach(function(sampleTitle) {
+ const samples = samplesByTitle[sampleTitle];
+ const samplesTrack = new tr.ui.tracks.SampleTrack(this.viewport);
+ samplesTrack.group = this.thread_;
+ samplesTrack.samples = samples;
+ samplesTrack.heading = this.thread_.userFriendlyName + ': ' +
+ sampleTitle;
+ samplesTrack.tooltip = this.thread_.userFriendlyDetails;
+ samplesTrack.selectionGenerator = function() {
+ const selection = new tr.model.EventSet();
+ for (let i = 0; i < samplesTrack.samples.length; i++) {
+ selection.push(samplesTrack.samples[i]);
+ }
+ return selection;
+ };
+ Polymer.dom(this).appendChild(samplesTrack);
+ }, this);
+ },
+
+ collapsedDidChange(collapsed) {
+ if (collapsed) {
+ let h = parseInt(this.tracks[0].height);
+ for (let i = 0; i < this.tracks.length; ++i) {
+ if (h > 2) {
+ this.tracks[i].height = Math.floor(h) + 'px';
+ } else {
+ this.tracks[i].style.display = 'none';
+ }
+ h = h * 0.5;
+ }
+ } else {
+ for (let i = 0; i < this.tracks.length; ++i) {
+ this.tracks[i].height = this.tracks[0].height;
+ this.tracks[i].style.display = '';
+ }
+ }
+ }
+ };
+
+ return {
+ ThreadTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html
new file mode 100644
index 00000000000..1ece1aa3f93
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/thread_track_test.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/model/event_set.html">
+<link rel="import" href="/tracing/model/instant_event.html">
+<link rel="import" href="/tracing/ui/base/dom_helpers.html">
+<link rel="import" href="/tracing/ui/timeline_track_view.html">
+<link rel="import" href="/tracing/ui/tracks/thread_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const HighlightInstantEvent = tr.model.ThreadHighlightInstantEvent;
+ const Process = tr.model.Process;
+ const EventSet = tr.model.EventSet;
+ const StackFrame = tr.model.StackFrame;
+ const Sample = tr.model.Sample;
+ const Thread = tr.model.Thread;
+ const ThreadSlice = tr.model.ThreadSlice;
+ const ThreadTrack = tr.ui.tracks.ThreadTrack;
+ const Viewport = tr.ui.TimelineViewport;
+ const newAsyncSlice = tr.c.TestUtils.newAsyncSlice;
+ const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed;
+ const newSliceEx = tr.c.TestUtils.newSliceEx;
+
+ test('selectionHitTestingWithThreadTrack', function() {
+ const model = new tr.Model();
+ const p1 = model.getOrCreateProcess(1);
+ const t1 = p1.getOrCreateThread(1);
+ t1.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4));
+ t1.sliceGroup.pushSlice(new ThreadSlice('', 'b', 0, 5.1, {}, 4));
+
+ const testEl = document.createElement('div');
+ Polymer.dom(testEl).appendChild(
+ tr.ui.b.createScopedStyle('heading { width: 100px; }'));
+ testEl.style.width = '600px';
+
+ const viewport = new Viewport(testEl);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(testEl).appendChild(drawingContainer);
+
+ const track = new ThreadTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ drawingContainer.updateCanvasSizeIfNeeded_();
+ track.thread = t1;
+
+ const y = track.getBoundingClientRect().top;
+ const h = track.getBoundingClientRect().height;
+ const wW = 10;
+ const vW = drawingContainer.canvas.getBoundingClientRect().width;
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.xSetWorldBounds(0, wW, vW);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ let selection = new EventSet();
+ const x = (1.5 / wW) * vW;
+ track.addIntersectingEventsInRangeToSelection(
+ x, x + 1, y, y + 1, selection);
+ assert.isTrue(selection.equals(
+ new EventSet([t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]])));
+
+ selection = new EventSet();
+ track.addIntersectingEventsInRangeToSelection(
+ (1.5 / wW) * vW, (1.8 / wW) * vW,
+ y, y + h, selection);
+ assert.isTrue(selection.equals(
+ new EventSet([t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]])));
+ });
+
+ test('filterThreadSlices', function() {
+ const model = new tr.Model();
+ const thread = new Thread(new Process(model, 7), 1);
+ thread.sliceGroup.pushSlice(newSliceEx(
+ {title: 'a', start: 0, duration: 0}));
+ thread.asyncSliceGroup.push(newAsyncSliceNamed('a', 0, 5, thread, thread));
+ const t = new ThreadTrack(new tr.ui.TimelineViewport());
+ t.thread = thread;
+
+ assert.strictEqual(t.tracks_.length, 2);
+ assert.instanceOf(t.tracks_[0], tr.ui.tracks.AsyncSliceGroupTrack);
+ assert.instanceOf(t.tracks_[1], tr.ui.tracks.SliceGroupTrack);
+ });
+
+ test('sampleThreadSlices', function() {
+ let thread;
+ let cpu;
+ const model = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(model) {
+ cpu = model.kernel.getOrCreateCpu(1);
+ thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+
+ const nodeA = tr.c.TestUtils.newProfileNode(model, 'a');
+ const nodeB = tr.c.TestUtils.newProfileNode(model, 'b', nodeA);
+ const nodeC = tr.c.TestUtils.newProfileNode(model, 'c', nodeB);
+ const nodeD = tr.c.TestUtils.newProfileNode(model, 'd', nodeA);
+
+ model.samples.push(new Sample(10, 'instructions_retired', nodeC, thread,
+ undefined, 10));
+ model.samples.push(new Sample(20, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(30, 'instructions_retired', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(40, 'instructions_retired', nodeD, thread,
+ undefined, 10));
+
+ model.samples.push(new Sample(25, 'page_fault', nodeB, thread,
+ undefined, 10));
+ model.samples.push(new Sample(35, 'page_fault', nodeD, thread,
+ undefined, 10));
+ }
+ });
+
+ const t = new ThreadTrack(new tr.ui.TimelineViewport());
+ t.thread = thread;
+ assert.strictEqual(t.tracks_.length, 2);
+
+ // Instructions retired
+ const t0 = t.tracks_[0];
+ assert.notEqual(t0.heading.indexOf('instructions_retired'), -1);
+ assert.instanceOf(t0, tr.ui.tracks.SampleTrack);
+ assert.strictEqual(t0.samples.length, 4);
+ t0.samples.forEach(function(s) {
+ assert.instanceOf(s, tr.model.Sample);
+ });
+
+ // page_fault
+ const t1 = t.tracks_[1];
+ assert.notEqual(t1.heading.indexOf('page_fault'), -1);
+ assert.instanceOf(t1, tr.ui.tracks.SampleTrack);
+ assert.strictEqual(t1.samples.length, 2);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css
new file mode 100644
index 00000000000..3d56eef5b8d
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.css
@@ -0,0 +1,33 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.track-button {
+ background-color: rgba(255, 255, 255, 0.5);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ color: rgba(0,0,0,0.2);
+ font-size: 10px;
+ height: 12px;
+ text-align: center;
+ width: 12px;
+}
+
+.track-button:hover {
+ background-color: rgba(255, 255, 255, 1.0);
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4);
+ color: rgba(0, 0, 0, 1);
+}
+
+.track-close-button {
+ left: 2px;
+ position: absolute;
+ top: 2px;
+}
+
+.track-collapse-button {
+ left: 3px;
+ position: absolute;
+ top: 2px;
+}
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html
new file mode 100644
index 00000000000..fccad427740
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/track.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="stylesheet" href="/tracing/ui/tracks/track.css">
+
+<link rel="import" href="/tracing/ui/base/container_that_decorates_its_children.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * The base class for all tracks, which render data into a provided div.
+ * @constructor
+ */
+ const Track = tr.ui.b.define('track',
+ tr.ui.b.ContainerThatDecoratesItsChildren);
+ Track.prototype = {
+ __proto__: tr.ui.b.ContainerThatDecoratesItsChildren.prototype,
+
+ decorate(viewport) {
+ tr.ui.b.ContainerThatDecoratesItsChildren.prototype.decorate.call(this);
+ if (viewport === undefined) {
+ throw new Error('viewport is required when creating a Track.');
+ }
+
+ this.viewport_ = viewport;
+ Polymer.dom(this).classList.add('track');
+ },
+
+ get viewport() {
+ return this.viewport_;
+ },
+
+ get drawingContainer() {
+ if (this instanceof tr.ui.tracks.DrawingContainer) return this;
+ let cur = this.parentElement;
+ while (cur) {
+ if (cur instanceof tr.ui.tracks.DrawingContainer) return cur;
+ cur = cur.parentElement;
+ }
+ return undefined;
+ },
+
+ get eventContainer() {
+ },
+
+ invalidateDrawingContainer() {
+ const dc = this.drawingContainer;
+ if (dc) dc.invalidate();
+ },
+
+ context() {
+ // This is a little weird here, but we have to be able to walk up the
+ // parent tree to get the context.
+ if (!Polymer.dom(this).parentNode) return undefined;
+
+ if (!Polymer.dom(this).parentNode.context) {
+ throw new Error('Parent container does not support context() method.');
+ }
+ return Polymer.dom(this).parentNode.context();
+ },
+
+ decorateChild_(childTrack) {
+ },
+
+ undecorateChild_(childTrack) {
+ if (childTrack.detach) {
+ childTrack.detach();
+ }
+ },
+
+ updateContents_() {
+ },
+
+ /**
+ * Wrapper function around draw() that performs transformations on the
+ * context necessary for the track's contents to be drawn in the right place
+ * given the current pan and zoom.
+ */
+ drawTrack(type) {
+ const ctx = this.context();
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ const bounds = this.getBoundingClientRect();
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+
+ ctx.save();
+ ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+ const dt = this.viewport.currentDisplayTransform;
+ const viewLWorld = dt.xViewToWorld(0);
+ const viewRWorld = dt.xViewToWorld(canvasBounds.width * pixelRatio);
+ const viewHeight = bounds.height * pixelRatio;
+
+ this.draw(type, viewLWorld, viewRWorld, viewHeight);
+ ctx.restore();
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ },
+
+ addEventsToTrackMap(eventToTrackMap) {
+ },
+
+ addContainersToTrackMap(containerToTrackMap) {
+ },
+
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loVY, hiVY, selection) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const dt = this.viewport.currentDisplayTransform;
+ const viewPixWidthWorld = dt.xViewVectorToWorld(1);
+ const loWX = dt.xViewToWorld(loVX * pixelRatio);
+ const hiWX = dt.xViewToWorld(hiVX * pixelRatio);
+
+ const clientRect = this.getBoundingClientRect();
+ const a = Math.max(loVY, clientRect.top);
+ const b = Math.min(hiVY, clientRect.bottom);
+ if (a > b) return;
+
+ this.addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection);
+ },
+
+ addIntersectingEventsInRangeToSelectionInWorldSpace(
+ loWX, hiWX, viewPixWidthWorld, selection) {
+ },
+
+ /**
+ * Gets implemented by supporting track types. The method adds the event
+ * closest to worldX to the selection.
+ *
+ * @param {number} worldX The position that is looked for.
+ * @param {number} worldMaxDist The maximum distance allowed from worldX to
+ * the event.
+ * @param {number} loY Lower Y bound of the search interval in view space.
+ * @param {number} hiY Upper Y bound of the search interval in view space.
+ * @param {Selection} selection Selection to which to add hits.
+ */
+ addClosestEventToSelection(
+ worldX, worldMaxDist, loY, hiY, selection) {
+ },
+
+ addClosestInstantEventToSelection(instantEvents, worldX,
+ worldMaxDist, selection) {
+ const instantEvent = tr.b.findClosestElementInSortedArray(
+ instantEvents,
+ function(x) { return x.start; },
+ worldX,
+ worldMaxDist);
+
+ if (!instantEvent) return;
+
+ selection.push(instantEvent);
+ }
+ };
+
+ return {
+ Track,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html
new file mode 100644
index 00000000000..620a35c8040
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track.html
@@ -0,0 +1,309 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/base/draw_helpers.html">
+<link rel="import" href="/tracing/ui/base/heading.html">
+<link rel="import" href="/tracing/ui/base/ui.html">
+<link rel="import" href="/tracing/ui/tracks/track.html">
+
+<style>
+.x-axis-track {
+ height: 12px;
+}
+
+.x-axis-track.tall-mode {
+ height: 30px;
+}
+</style>
+
+<script>
+'use strict';
+
+tr.exportTo('tr.ui.tracks', function() {
+ /**
+ * A track that displays the x-axis.
+ * @constructor
+ * @extends {Track}
+ */
+ const XAxisTrack = tr.ui.b.define('x-axis-track', tr.ui.tracks.Track);
+
+ XAxisTrack.prototype = {
+ __proto__: tr.ui.tracks.Track.prototype,
+
+ decorate(viewport) {
+ tr.ui.tracks.Track.prototype.decorate.call(this, viewport);
+ Polymer.dom(this).classList.add('x-axis-track');
+ this.strings_secs_ = [];
+ this.strings_msecs_ = [];
+ this.strings_usecs_ = [];
+ this.strings_nsecs_ = [];
+
+ this.viewportChange_ = this.viewportChange_.bind(this);
+ viewport.addEventListener('change', this.viewportChange_);
+
+ const heading = document.createElement('tr-ui-b-heading');
+ heading.arrowVisible = false;
+ Polymer.dom(this).appendChild(heading);
+ },
+
+ detach() {
+ tr.ui.tracks.Track.prototype.detach.call(this);
+ this.viewport.removeEventListener('change',
+ this.viewportChange_);
+ },
+
+ viewportChange_() {
+ if (this.viewport.interestRange.isEmpty) {
+ Polymer.dom(this).classList.remove('tall-mode');
+ } else {
+ Polymer.dom(this).classList.add('tall-mode');
+ }
+ },
+
+ draw(type, viewLWorld, viewRWorld, viewHeight) {
+ switch (type) {
+ case tr.ui.tracks.DrawType.GRID:
+ this.drawGrid_(viewLWorld, viewRWorld);
+ break;
+ case tr.ui.tracks.DrawType.MARKERS:
+ this.drawMarkers_(viewLWorld, viewRWorld);
+ break;
+ }
+ },
+
+ drawGrid_(viewLWorld, viewRWorld) {
+ const ctx = this.context();
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ const canvasBounds = ctx.canvas.getBoundingClientRect();
+ const trackBounds = this.getBoundingClientRect();
+ const width = canvasBounds.width * pixelRatio;
+ const height = trackBounds.height * pixelRatio;
+
+ const hasInterestRange = !this.viewport.interestRange.isEmpty;
+
+ const xAxisHeightPx = hasInterestRange ? (height * 2) / 5 : height;
+
+ const vp = this.viewport;
+ const dt = vp.currentDisplayTransform;
+
+ vp.updateMajorMarkData(viewLWorld, viewRWorld);
+ const majorMarkDistanceWorld = vp.majorMarkWorldPositions.length > 1 ?
+ vp.majorMarkWorldPositions[1] - vp.majorMarkWorldPositions[0] : 0;
+
+ const numTicksPerMajor = 5;
+ const minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
+ const minorMarkDistancePx = dt.xWorldVectorToView(minorMarkDistanceWorld);
+
+ const minorTickHeight = Math.floor(xAxisHeightPx * 0.25);
+
+ ctx.save();
+
+ ctx.lineWidth = Math.round(pixelRatio);
+
+ // Apply subpixel translate to get crisp lines.
+ // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+ const crispLineCorrection = (ctx.lineWidth % 2) / 2;
+ ctx.translate(crispLineCorrection, -crispLineCorrection);
+
+ ctx.fillStyle = 'rgb(0, 0, 0)';
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'top';
+
+ ctx.font = (9 * pixelRatio) + 'px sans-serif';
+
+ const tickLabels = [];
+ ctx.beginPath();
+ for (let i = 0; i < vp.majorMarkWorldPositions.length; i++) {
+ const curXWorld = vp.majorMarkWorldPositions[i];
+ const curXView = dt.xWorldToView(curXWorld);
+ const displayText = vp.majorMarkUnit.format(
+ curXWorld, {deltaValue: majorMarkDistanceWorld});
+ ctx.fillText(displayText, curXView + (2 * pixelRatio), 0);
+
+ // Draw major mark.
+ tr.ui.b.drawLine(ctx, curXView, 0, curXView, xAxisHeightPx);
+
+ // Draw minor marks.
+ if (minorMarkDistancePx) {
+ for (let j = 1; j < numTicksPerMajor; ++j) {
+ const xView = Math.floor(curXView + minorMarkDistancePx * j);
+ tr.ui.b.drawLine(ctx,
+ xView, xAxisHeightPx - minorTickHeight,
+ xView, xAxisHeightPx);
+ }
+ }
+ }
+
+ // Draw bottom bar.
+ ctx.strokeStyle = 'rgb(0, 0, 0)';
+ tr.ui.b.drawLine(ctx, 0, height, width, height);
+ ctx.stroke();
+
+ // Give distance between directly adjacent markers.
+ if (!hasInterestRange) return;
+
+ // Draw middle bar.
+ tr.ui.b.drawLine(ctx, 0, xAxisHeightPx, width, xAxisHeightPx);
+ ctx.stroke();
+
+ // Distance Variables.
+ let displayDistance;
+ const displayTextColor = 'rgb(0,0,0)';
+
+ // Arrow Variables.
+ const arrowSpacing = 10 * pixelRatio;
+ const arrowColor = 'rgb(128,121,121)';
+ const arrowPosY = xAxisHeightPx * 1.75;
+ const arrowWidthView = 3 * pixelRatio;
+ const arrowLengthView = 10 * pixelRatio;
+ const spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing);
+
+ ctx.textBaseline = 'middle';
+ ctx.font = (14 * pixelRatio) + 'px sans-serif';
+ const textPosY = arrowPosY;
+
+ const interestRange = vp.interestRange;
+
+ // If the range is zero, draw it's min timestamp next to the line.
+ if (interestRange.range === 0) {
+ const markerWorld = interestRange.min;
+ const markerView = dt.xWorldToView(markerWorld);
+
+ const textToDraw = vp.majorMarkUnit.format(markerWorld);
+ let textLeftView = markerView + 4 * pixelRatio;
+ const textWidthView = ctx.measureText(textToDraw).width;
+
+ // Put text to the left in case it gets cut off.
+ if (textLeftView + textWidthView > width) {
+ textLeftView = markerView - 4 * pixelRatio - textWidthView;
+ }
+
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+ return;
+ }
+
+ const leftMarker = interestRange.min;
+ const rightMarker = interestRange.max;
+
+ const leftMarkerView = dt.xWorldToView(leftMarker);
+ const rightMarkerView = dt.xWorldToView(rightMarker);
+
+ const distanceBetweenMarkers = interestRange.range;
+ const distanceBetweenMarkersView =
+ dt.xWorldVectorToView(distanceBetweenMarkers);
+ const positionInMiddleOfMarkersView =
+ leftMarkerView + (distanceBetweenMarkersView / 2);
+
+ const textToDraw = vp.majorMarkUnit.format(distanceBetweenMarkers);
+ const textWidthView = ctx.measureText(textToDraw).width;
+ const spaceForArrowsAndTextView =
+ textWidthView + spaceForArrowsView + arrowSpacing;
+
+ // Set text positions.
+ let textLeftView = positionInMiddleOfMarkersView - textWidthView / 2;
+ const textRightView = textLeftView + textWidthView;
+
+ if (spaceForArrowsAndTextView > distanceBetweenMarkersView) {
+ // Print the display distance text right of the 2 markers.
+ textLeftView = rightMarkerView + 2 * arrowSpacing;
+
+ // Put text to the left in case it gets cut off.
+ if (textLeftView + textWidthView > width) {
+ textLeftView = leftMarkerView - 2 * arrowSpacing - textWidthView;
+ }
+
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+
+ // Draw the arrows pointing from outside in and a line in between.
+ ctx.strokeStyle = arrowColor;
+ ctx.beginPath();
+ tr.ui.b.drawLine(ctx, leftMarkerView, arrowPosY, rightMarkerView,
+ arrowPosY);
+ ctx.stroke();
+
+ ctx.fillStyle = arrowColor;
+ tr.ui.b.drawArrow(ctx,
+ leftMarkerView - 1.5 * arrowSpacing, arrowPosY,
+ leftMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ tr.ui.b.drawArrow(ctx,
+ rightMarkerView + 1.5 * arrowSpacing, arrowPosY,
+ rightMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ } else if (spaceForArrowsView <= distanceBetweenMarkersView) {
+ let leftArrowStart;
+ let rightArrowStart;
+ if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) {
+ // Print the display distance text.
+ ctx.fillStyle = displayTextColor;
+ ctx.fillText(textToDraw, textLeftView, textPosY);
+
+ leftArrowStart = textLeftView - arrowSpacing;
+ rightArrowStart = textRightView + arrowSpacing;
+ } else {
+ leftArrowStart = positionInMiddleOfMarkersView;
+ rightArrowStart = positionInMiddleOfMarkersView;
+ }
+
+ // Draw the arrows pointing inside out.
+ ctx.strokeStyle = arrowColor;
+ ctx.fillStyle = arrowColor;
+ tr.ui.b.drawArrow(ctx,
+ leftArrowStart, arrowPosY,
+ leftMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ tr.ui.b.drawArrow(ctx,
+ rightArrowStart, arrowPosY,
+ rightMarkerView, arrowPosY,
+ arrowLengthView, arrowWidthView);
+ }
+
+ ctx.restore();
+ },
+
+ drawMarkers_(viewLWorld, viewRWorld) {
+ const pixelRatio = window.devicePixelRatio || 1;
+ const trackBounds = this.getBoundingClientRect();
+ const viewHeight = trackBounds.height * pixelRatio;
+
+ if (!this.viewport.interestRange.isEmpty) {
+ this.viewport.interestRange.draw(this.context(),
+ viewLWorld, viewRWorld, viewHeight);
+ }
+ },
+
+ /**
+ * Adds items intersecting the given range to a selection.
+ * @param {number} loVX Lower X bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVX Upper X bound of the interval to search, in
+ * viewspace.
+ * @param {number} loVY Lower Y bound of the interval to search, in
+ * viewspace.
+ * @param {number} hiVY Upper Y bound of the interval to search, in
+ * viewspace.
+ * @param {Selection} selection Selection to which to add results.
+ */
+ addIntersectingEventsInRangeToSelection(
+ loVX, hiVX, loY, hiY, selection) {
+ // Does nothing. There's nothing interesting to pick on the xAxis
+ // track.
+ },
+
+ addAllEventsMatchingFilterToSelection(filter, selection) {
+ }
+ };
+
+ return {
+ XAxisTrack,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html
new file mode 100644
index 00000000000..459c05cd122
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/tracks/x_axis_track_test.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/core/test_utils.html">
+<link rel="import" href="/tracing/ui/timeline_viewport.html">
+<link rel="import" href="/tracing/ui/tracks/drawing_container.html">
+<link rel="import" href="/tracing/ui/tracks/x_axis_track.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ test('instantiate', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_interestRange', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.interestRange.min = 300;
+ viewport.interestRange.max = 300;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ test('instantiate_singlePointInterestRange', function() {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.interestRange.min = 300;
+ viewport.interestRange.max = 400;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ this.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+ });
+
+ function testTimeMode(mode, testInstance, numDigits, opt_unit) {
+ const div = document.createElement('div');
+
+ const viewport = new tr.ui.TimelineViewport(div);
+ viewport.timeMode = mode;
+ const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport);
+ Polymer.dom(div).appendChild(drawingContainer);
+
+ const trackContext = drawingContainer.ctx_;
+ const oldFillText = trackContext.fillText;
+ const fillTextText = [];
+ const fillTextThis = [];
+ trackContext.fillText = function(text, xPos, yPos) {
+ fillTextText.push(text);
+ fillTextThis.push(this);
+ return oldFillText.call(this, text, xPos, yPos);
+ };
+
+ const track = tr.ui.tracks.XAxisTrack(viewport);
+ Polymer.dom(drawingContainer).appendChild(track);
+ testInstance.addHTMLOutput(div);
+
+ drawingContainer.invalidate();
+ tr.b.forceAllPendingTasksToRunForTest();
+
+ const dt = new tr.ui.TimelineDisplayTransform();
+ dt.setPanAndScale(0, track.clientWidth / 1000);
+ track.viewport.setDisplayTransformImmediately(dt);
+
+ const formatter =
+ new Intl.NumberFormat(undefined, { numDigits, numDigits });
+ const formatFunction = function(value) {
+ let valueString = value.toLocaleString(undefined, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: numDigits
+ });
+ if (opt_unit) valueString += opt_unit;
+ return valueString;
+ };
+ const expectedText = viewport.majorMarkWorldPositions.map(
+ formatFunction);
+ assert.strictEqual(fillTextText.length, fillTextThis.length);
+ for (let i = 0; i < fillTextText.length; i++) {
+ assert.deepEqual(fillTextText[i], expectedText[i]);
+ assert.strictEqual(fillTextThis[i], trackContext);
+ }
+ }
+
+ test('instantiate_timeModeMs', function() {
+ testTimeMode(tr.ui.TimelineViewport.TimeMode.TIME_IN_MS,
+ this, 3, ' ms');
+ });
+
+ test('instantiate_timeModeRevisions', function() {
+ testTimeMode(tr.ui.TimelineViewport.TimeMode.REVISIONS, this, 0);
+ });
+});
+</script>
+
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html
new file mode 100644
index 00000000000..75b616332c2
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+
+<!--
+This element handles storing and retrieving the brushing state of arbitrary
+views (e.g. analysis sub-views). An element can use it by instantiating it and
+appending it to itself:
+
+ <div id="some-view-with-specific-brushing-state">
+ <tr-ui-b-view-specific-brushing-state view-id="unique-view-identifier">
+ </tr-ui-b-view-specific-brushing-state>
+ ... other child elements ...
+ </div>
+
+The state can then be retrieved from and pushed to the state element as
+follows:
+
+ newStateElement.set(state);
+ state = newStateElement.get();
+
+Under the hood, the state element searches the DOM tree for an ancestor element
+with a brushingStateController field to persist the state (see the
+tr.c.BrushingStateController and tr.ui.b.BrushingState classes for more
+details).
+-->
+<dom-module id='tr-ui-b-view-specific-brushing-state'>
+ <template></template>
+</dom-module>
+<script>
+'use strict';
+
+Polymer({
+ is: 'tr-ui-b-view-specific-brushing-state',
+
+ /** Compulsory unique identifier of the associated view. */
+ get viewId() {
+ return this.getAttribute('view-id');
+ },
+
+ set viewId(viewId) {
+ Polymer.dom(this).setAttribute('view-id', viewId);
+ },
+
+ /**
+ * Retrieve the persisted state of the associated view. The returned object
+ * (or any of its fields) must not be modified by the caller (unless the
+ * object/field is treated as a reference).
+ *
+ * If no state has been persisted yet or there is no ancestor element with
+ * a brushingStateController field, this method returns undefined.
+ */
+ get() {
+ const viewId = this.viewId;
+ if (!viewId) {
+ throw new Error('Element must have a view-id attribute!');
+ }
+
+ const brushingStateController =
+ tr.c.BrushingStateController.getControllerForElement(this);
+ if (!brushingStateController) return undefined;
+
+ return brushingStateController.getViewSpecificBrushingState(viewId);
+ },
+
+ /**
+ * Persist the provided state of the associated view. The provided object
+ * (or any of its fields) must not be modified afterwards (unless the
+ * object/field is treated as a reference).
+ *
+ * If there is no ancestor element with a brushingStateController field,
+ * this method does nothing.
+ */
+ set(state) {
+ const viewId = this.viewId;
+ if (!viewId) {
+ throw new Error('Element must have a view-id attribute!');
+ }
+
+ const brushingStateController =
+ tr.c.BrushingStateController.getControllerForElement(this);
+ if (!brushingStateController) return;
+
+ brushingStateController.changeViewSpecificBrushingState(viewId, state);
+ }
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html
new file mode 100644
index 00000000000..ee920cb1100
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/ui/view_specific_brushing_state_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/ui/brushing_state_controller.html">
+<link rel="import" href="/tracing/ui/view_specific_brushing_state.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const BrushingStateController = tr.c.BrushingStateController;
+
+ function setStateElement(containerEl, viewId) {
+ const stateElement = document.createElement(
+ 'tr-ui-b-view-specific-brushing-state');
+ stateElement.viewId = viewId;
+ Polymer.dom(containerEl).appendChild(stateElement);
+ return stateElement;
+ }
+
+ function addChildDiv(element) {
+ const child = element.ownerDocument.createElement('div');
+ Polymer.dom(element).appendChild(child);
+ return child;
+ }
+
+ function addShadowChildDiv(element) {
+ const shadowRoot = element.createShadowRoot();
+ return addChildDiv(shadowRoot);
+ }
+
+ test('instantiate_withoutBrushingStateController', function() {
+ const containerEl = document.createElement('div');
+
+ const stateElement1 = setStateElement(containerEl, 'test-view');
+ assert.isUndefined(stateElement1.get());
+ stateElement1.set({e: 2.71828});
+ assert.isUndefined(stateElement1.get());
+ });
+
+ test('instantiate_withBrushingStateController', function() {
+ const rootEl = document.createElement('div');
+ const containerEl = addChildDiv(addShadowChildDiv(addChildDiv(rootEl)));
+ rootEl.brushingStateController = new BrushingStateController(undefined);
+
+ const stateElement1 = setStateElement(containerEl, 'test-view');
+ assert.isUndefined(stateElement1.get());
+ stateElement1.set({e: 2.71828});
+ assert.deepEqual(stateElement1.get(), {e: 2.71828});
+
+ const stateElement2 = setStateElement(containerEl, 'test-view-2');
+ assert.isUndefined(stateElement2.get());
+ stateElement2.set({pi: 3.14159});
+ assert.deepEqual(stateElement2.get(), {pi: 3.14159});
+
+ const stateElement3 = setStateElement(containerEl, 'test-view');
+ assert.deepEqual(stateElement3.get(), {e: 2.71828});
+
+ const stateElement4 = setStateElement(containerEl, 'test-view-2');
+ assert.deepEqual(stateElement4.get(), {pi: 3.14159});
+ });
+});
+</script>