Switch to browserify for better iteration time

This commit is contained in:
Jamie Wong 2017-12-14 00:30:25 -08:00
parent f6772e3498
commit 355ceb8c6c
15 changed files with 117 additions and 387 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
node_modules
dist/speedscope-dev.js

73
aphrodite.d.ts vendored Normal file
View File

@ -0,0 +1,73 @@
// Type definitions for Aphrodite 0.5.0
// Project: https://github.com/Khan/aphrodite
// Definitions by: Alexey Svetliakov <https://github.com/asvetliakov>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
declare module 'aphrodite' {
// Modified to remove dependency on 'react'
/**
* Aphrodite style declaration
*/
export interface StyleDeclaration {
[key: string]: any
}
interface StyleSheetStatic {
/**
* Create style sheet
*/
create<T extends StyleDeclaration>(styles: T): T;
/**
* Rehydrate class names from server renderer
*/
rehydrate(renderedClassNames: string[]): void;
}
export var StyleSheet: StyleSheetStatic;
/**
* Get class names from passed styles
*/
export function css(...styles: any[]): string;
interface StaticRendererResult {
html: string;
css: {
content: string;
renderedClassNames: string[];
}
}
/**
* Utilities for using Aphrodite server-side.
*/
interface StyleSheetServerStatic {
renderStatic(renderFunc: () => string): StaticRendererResult;
}
export var StyleSheetServer: StyleSheetServerStatic;
interface StyleSheetTestUtilsStatic {
/**
* Prevent styles from being injected into the DOM.
*
* This is useful in situations where you'd like to test rendering UI
* components which use Aphrodite without any of the side-effects of
* Aphrodite happening. Particularly useful for testing the output of
* components when you have no DOM, e.g. testing in Node without a fake DOM.
*
* Should be paired with a subsequent call to
* clearBufferAndResumeStyleInjection.
*/
suppressStyleInjection(): void;
/**
* Opposite method of preventStyleInject.
*/
clearBufferAndResumeStyleInjection(): void;
}
export var StyleSheetTestUtils: StyleSheetTestUtilsStatic;
}

2
dist/speedscope.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,10 @@
import {h, render, Component} from 'preact'
import {h, Component} from 'preact'
import {StyleSheet, css} from 'aphrodite'
import {Profile, Frame, CallTreeNode} from './profile'
import regl, {vec2, vec3, mat3, ReglCommand, ReglCommandConstructor} from 'regl'
import { Rect, Vec2, AffineTransform, clamp } from './math'
import * as regl from 'regl'
import { vec2, vec3, ReglCommand } from 'regl'
import { Rect, Vec2, AffineTransform } from './math'
import { atMostOnceAFrame } from "./utils";
enum FontFamily {
@ -105,9 +106,7 @@ export class Flamechart {
const aParts = parts(a)
const bParts = parts(b)
const matching = 0
const minLength = Math.min(aParts.length, bParts.length)
const maxLength = Math.max(aParts.length, bParts.length)
let prefixMatchLength = 0
for (let i = 0; i < minLength; i++) {
@ -196,7 +195,6 @@ const ELLIPSIS = '\u2026'
function buildTrimmedText(text: string, length: number) {
const prefixLength = Math.floor(length / 2)
const suffixLength = Math.ceil(length / 2)
const prefix = text.substr(0, prefixLength)
const suffix = text.substr(text.length - prefixLength, prefixLength)
return prefix + ELLIPSIS + suffix
@ -212,7 +210,7 @@ function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: string): nu
function trimTextMid(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) return text
const [lo, hi] = binarySearch(0, text.length, (n) => {
const [lo,] = binarySearch(0, text.length, (n) => {
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
}, maxWidth)
return buildTrimmedText(text, lo)
@ -246,7 +244,7 @@ interface FlamechartPanZoomViewProps {
setNodeHover: (node: CallTreeNode | null, logicalViewSpacemous: Vec2) => void
}
export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps, void> {
export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps, {}> {
renderer: ReglCommand<RectangleBatchRendererProps> | null = null
ctx: WebGLRenderingContext | null = null
@ -265,9 +263,6 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
const colors: vec3[] = []
const layers = flamechart.getLayers()
const duration = flamechart.getDuration()
const maxStackHeight = layers.length
const frameColors = flamechart.getFrameColors()
this.labels = []
@ -329,7 +324,6 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
}
private LOGICAL_VIEW_SPACE_FRAME_HEIGHT = 16
private LOGICAL_VIEW_SPACE_LABEL_FONT_SIZE = 12
private configSpaceToPhysicalViewSpace() {
return AffineTransform.betweenRects(
@ -564,8 +558,6 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (!configSpaceMouse) return
let labelUnderMouse: FlamechartFrameLabel | null = null
// This could be sped up significantly
for (let label of this.labels) {
if (label.configSpaceBounds.contains(configSpaceMouse)) {
@ -718,11 +710,9 @@ export class FlamechartView extends Component<FlamechartViewProps, FlamechartVie
)
}
containerRef = (container: HTMLDivElement) => { this.container = container }
containerRef = (container?: Element) => { this.container = container as HTMLDivElement || null }
render() {
const { flamechart } = this.props
return (
<div className={css(style.fill, style.clip)} ref={this.containerRef}>
<FlamechartPanZoomView

View File

@ -5,50 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>SpeedScope</title>
<style>
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
<link rel="stylesheet" href="reset.css">
</style>
</head>
<body>

View File

@ -6,14 +6,12 @@ import {importFromBGFlameGraph} from './import/bg-flamegraph'
import {Profile} from './profile'
import {Flamechart, FlamechartView} from './flamechart'
import { request } from 'https';
interface ApplicaionState {
interface ApplicationState {
profile: Profile | null
flamechart: Flamechart | null
}
class Application extends Component<{}, ApplicaionState> {
class Application extends Component<{}, ApplicationState> {
onDrop = (ev: DragEvent) => {
const reader = new FileReader
reader.addEventListener('loadend', () => {

View File

@ -4,26 +4,19 @@
"description": "",
"main": "index.js",
"scripts": {
"serve": "./serveit -i node_modules 'node_modules/.bin/rollup -c rollup.config.dev.js'",
"release": "rollup -c rollup.config.release.js"
"serve": "budo index.tsx --force-default-index --css reset.css -- -p tsify",
"release": "browserify index.tsx -p tsify | uglifyjs -cm > dist/speedscope.js"
},
"author": "",
"license": "MIT",
"devDependencies": {
"@types/aphrodite": "^0.5.6",
"@types/react-dom": "^16.0.3",
"aphrodite": "^1.2.5",
"preact": "^8.2.6",
"react": "^16.1.1",
"react-dom": "^16.1.1",
"browserify": "^14.5.0",
"budo": "^10.0.4",
"preact": "^8.2.7",
"regl": "^1.3.0",
"rollup": "^0.51.5",
"rollup-plugin-commonjs": "^8.2.6",
"rollup-plugin-node-globals": "^1.1.0",
"rollup-plugin-node-resolve": "^3.0.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-typescript": "^0.8.1",
"rollup-plugin-uglify-es": "0.0.1",
"typescript": "^2.6.1"
"tsify": "^3.0.4",
"typescript": "^2.6.1",
"uglify-es": "^3.2.2"
}
}

View File

@ -115,6 +115,9 @@ export class Profile {
this.duration = duration
}
getDuration() { return this.duration }
getEvents() { return this.events }
forEachSample(fn: (stack: CallTreeNode[], timeDelta: number) => void) {
const nodeToStack = new Map<CallTreeNode, CallTreeNode[]>()
for (let i = 0; i < this.samples.length; i++) {

32
regl.d.ts vendored
View File

@ -10,10 +10,6 @@ declare module "regl" {
type ReglValue<P> = ReglPrimitiveValue | {(context: any, props: P, batchId: number): ReglPrimitiveValue}
export type vec2 = [number, number]
export type vec3 = [number, number, number]
export type mat3 = [number, number, number, number, number, number, number, number, number]
interface ReglCommandParameters<P> {
/** Source code of vertex shader */
vert: string
@ -35,21 +31,25 @@ declare module "regl" {
offset?: number
}
export interface ReglCommand<P> {
(p: P): void
}
export interface ReglCommandConstructor {
<P>(params: ReglCommandParameters<P>): ReglCommand<P>
}
function ReglConstructor(): ReglCommandConstructor
function ReglConstructor(ctx: WebGLRenderingContext): ReglCommandConstructor
function ReglConstructor(canvas: HTMLCanvasElement): ReglCommandConstructor
function ReglConstructor(): ReglConstructor.ReglCommandConstructor
function ReglConstructor(ctx: WebGLRenderingContext): ReglConstructor.ReglCommandConstructor
function ReglConstructor(canvas: HTMLCanvasElement): ReglConstructor.ReglCommandConstructor
namespace ReglConstructor {
function prop(name: string): ReglProp
export type vec2 = [number, number]
export type vec3 = [number, number, number]
export type mat3 = [number, number, number, number, number, number, number, number, number]
interface ReglCommand<P> {
(p: P): void
}
interface ReglCommandConstructor {
<P>(params: ReglCommandParameters<P>): ReglCommand<P>
}
}
export default ReglConstructor
export = ReglConstructor
}

View File

@ -1,11 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>SpeedScope</title>
<style>
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
@ -48,11 +44,4 @@ q:before, q:after {
table {
border-collapse: collapse;
border-spacing: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="dist/speedscope-dev.js"></script>
</body>
</html>
}

View File

@ -1,42 +0,0 @@
import commonjs from 'rollup-plugin-commonjs';
import nodeResolve from 'rollup-plugin-node-resolve';
import typescript from 'rollup-plugin-typescript';
import replace from 'rollup-plugin-replace';
import nodeGlobals from 'rollup-plugin-node-globals';
import uglify from 'rollup-plugin-uglify-es';
export function config(isRelease) {
const plugins = [
typescript({
typescript: require('typescript')
}),
nodeResolve({
jsnext: true,
main: true
}),
commonjs({
include: 'node_modules/**',
namedExports: {
'aphrodite': ['StyleSheet', 'css'],
'react-dom': ['render'],
'react': ['Component', 'createElement']
},
ignore: [ 'domain' ]
}),
replace({
'process.env.NODE_ENV': JSON.stringify(isRelease ? 'prod' : 'development')
}),
nodeGlobals()
]
if (isRelease) plugins.push(uglify())
return [{
input: 'index.tsx',
output: {
file: isRelease ? 'dist/speedscope.js' : 'dist/speedscope-dev.js',
format: 'iife'
},
plugins: plugins
}]
}

View File

@ -1,2 +0,0 @@
import {config} from './rollup.config.common.js'
export default config(false)

View File

@ -1,2 +0,0 @@
import {config} from './rollup.config.common.js'
export default config(true)

227
serveit
View File

@ -1,227 +0,0 @@
#!/usr/bin/env ruby
# Copyright (c) 2015 Gary Bernhardt
#
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require "find"
require "webrick"
require "open3"
require "optparse"
class ServeIt
VERSION = [0, 0, 2]
def self.main
serve_dir, ignored_paths, command = parse_opts
serve_dir = File.expand_path(serve_dir)
Server.new(serve_dir, ignored_paths, command).serve
end
def self.parse_opts
options = {:serve_dir => ".",
:ignored_paths => []}
parser = OptionParser.new do |opts|
opts.banner = "Usage: #{$PROGRAM_NAME} [options] command"
opts.on_tail("-s", "--serve-dir DIR", "Root directory for server") do |dir|
options[:serve_dir] = dir
end
opts.on_tail("-i", "--ignore PATH", "Ignore changes to file or directory") do |path|
options[:ignored_paths] << path
end
opts.on_tail("--version", "Show version") do |dir|
puts ServeIt::VERSION.join('.')
exit
end
end
begin
parser.parse!(ARGV)
rescue OptionParser::InvalidOption => e
$stderr.puts e
$stderr.puts parser
exit 1
end
if ARGV.count == 0
command = nil
elsif ARGV.count == 1
command = ARGV.fetch(0)
else
$stderr.write parser.to_s
exit 1
end
[options.fetch(:serve_dir), options.fetch(:ignored_paths), command]
end
class Server
def initialize(serve_dir, ignored_paths, command)
@mutex = Mutex.new
@serve_dir = serve_dir
@command = command
@rebuilder = Rebuilder.new(@command, ignored_paths) if @command
end
def serve
port = 8000
puts "Starting server at http://localhost:#{port}"
server = WEBrick::HTTPServer.new(:Port => port)
server.mount_proc '/' do |req, res|
relative_path = req.path.sub(/^\//, '')
local_abs_path = File.absolute_path(relative_path, @serve_dir)
if relative_path == "favicon.ico"
respond_to_favicon(res)
else
respond_to_path(res, relative_path, local_abs_path)
end
end
trap 'INT' do server.shutdown end
server.start
end
def respond_to_favicon(res)
res.status = 404
end
def respond_to_path(res, relative_path, local_abs_path)
begin
rebuild_if_needed
rescue Rebuilder::RebuildFailed => e
return respond_to_error(res, e.to_s)
end
if File.directory?(local_abs_path)
respond_to_dir(res, relative_path, local_abs_path)
else
# We're building a file
respond_to_file(res, local_abs_path)
end
end
def respond_to_error(res, message)
res.content_type = "text/html"
res.body = "<pre>" + message + "</pre>"
end
def respond_to_dir(res, rel_path, local_abs_path)
res.content_type = "text/html"
res.body = (
"<p><h3>Listing for /#{rel_path}</h3></p>\n" +
Dir.entries(local_abs_path).select do |child|
child != "."
end.sort.map do |child|
full_child_path_on_server = File.join("/", rel_path, child)
%{<a href="#{full_child_path_on_server}">#{child}</a><br>}
end.join("\n")
)
end
def respond_to_file(res, local_abs_path)
res.body = File.read(local_abs_path)
res.content_type = guess_content_type(local_abs_path)
end
def guess_content_type(path)
extension = File.extname(path).sub(/^\./, '')
WEBrick::HTTPUtils::DefaultMimeTypes.fetch(extension) do
"application/octet-stream"
end
end
def rebuild_if_needed
# Webrick is multi-threaded; guard against concurrent builds
@mutex.synchronize do
if @rebuilder
@rebuilder.rebuild_if_needed
end
end
end
end
class Rebuilder
def initialize(command, ignored_paths)
@command = command
@ignored_paths = ignored_paths
@last_disk_state = nil
end
def rebuild_if_needed
if disk_state != @last_disk_state
stdout_and_stderr, success = rebuild
if !success
message = "Failed to build! Command output:\n\n" + stdout_and_stderr
raise RebuildFailed.new(message)
end
# Get a new post-build disk state so we don't pick up changes made during
# the build.
@last_disk_state = disk_state
[stdout_and_stderr, success]
end
end
def rebuild
puts "Running command: #{@command}"
puts " begin build".rjust(80, "=")
start_time = Time.now
stdout_and_stderr, status = Open3.capture2e(@command)
print stdout_and_stderr
puts (" built in %.03fs" % (Time.now - start_time)).rjust(80, "=")
[stdout_and_stderr, status.success?]
end
def disk_state
start_time = Time.now
paths = []
Find.find(".") do |path|
if ignore_path?(path)
Find.prune
else
paths << path
end
end
paths.map do |path|
[path, File.stat(path).mtime.to_s]
end.sort.tap do
puts (" scanned in %.03fs" % (Time.now - start_time)).rjust(80, "=")
end
end
def ignore_path?(path)
@ignored_paths.any? do |ignored_path|
File.absolute_path(path) == File.absolute_path(ignored_path)
end
end
class RebuildFailed < RuntimeError; end
end
end
if __FILE__ == $PROGRAM_NAME
ServeIt.main
end

View File

@ -3,6 +3,7 @@
"module": "commonjs",
"strict": true,
"jsx": "react",
"noUnusedLocals": true,
"jsxFactory": "h",
"target": "es2015"
}