// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // Script that generates different approaches to initialize classes in // JavaScript. // Also benchmarks the approaches. import 'dart:io'; import 'dart:async'; class Config { /// Number of classes that should be generated. final int nbClasses; /// Number of methods per class. final int nbMethodsPerClass; /// Should the JavaScript classes share a common super class? /// /// Currently unused for Dart, since it always has a common super class /// anyways. // TODO(floitsch): also create a common super class in Dart? final bool shareCommonSuperclass; /// Assign unique names to the methods or let them share the same one? /// /// Independent of this flag, the `callAll` and `instantiatePrevious` method /// names are the same for all classes. final bool sameMethodNames; /// Adds a `print` statement to the method. final bool shouldPrintInMethod; /// Adds while loops to the method body. /// /// This has the effect that dart2js won't be able to inline the method and /// controls the size of the method bodies. final int nbWhileLoopsInBody; /// Should the JavaScript output be wrapped into an anonymous function? /// /// When enabled wraps the program with the following pattern: /// `(function() { })()`. final bool shouldWrapProgram; /// Adds a `callAll` method that invokes all other methods of the class. /// /// This is necessary for dart2js to avoid tree-shaking. /// Should probably always be on (except for presentations to demonstrate that /// dart2js knows how to tree-shake). /// /// This method counts towards the [nbMethodsPerClass] limit. final bool shouldEmitCallAllMethods; /// Adds an `instantiatePrevious` method that instantiates the previous class. /// /// A "previous" class is the class that was generated before the current /// class. The first class returns `null`. final bool shouldEmitInstantiatePreviousMethod; /// Makes sure that the dart2js tree-shaker doesn't remove classes. /// /// When set to `-1`, all classes are kept alive. final int fakeInstantiateClass; /// Defines the percent of classes that are dynamically instantiated. final int instantiateClassesPercent; Config( {this.nbClasses, this.nbMethodsPerClass, this.shareCommonSuperclass, this.sameMethodNames, this.shouldPrintInMethod, this.nbWhileLoopsInBody, this.shouldWrapProgram, this.shouldEmitCallAllMethods, this.shouldEmitInstantiatePreviousMethod, this.fakeInstantiateClass, this.instantiateClassesPercent}); } String get d8Path { Uri scriptPath = Platform.script; String d8Executable = "../../../third_party/d8/"; if (Platform.isWindows) { d8Executable += "windows/d8.exe"; } else if (Platform.isMacOS) { d8Executable += "macos/d8"; } else if (Platform.isLinux) { d8Executable += "linux/d8"; } else { return null; } return scriptPath.resolve(d8Executable).path; } String get jsShellPath { Uri scriptPath = Platform.script; if (!Platform.isLinux) { return null; } return scriptPath.resolve("../../../tools/testing/bin/jsshell").path; } String get dart2jsPath { Uri scriptPath = Platform.script; return scriptPath.resolve("../../../sdk/bin/dart2js").path; } abstract class ClassGenerator { final StringBuffer buffer = new StringBuffer(); // By convention all methods should take one argument with this name. final String argumentName = "x"; final String callOtherMethodsName = "callAll"; final String instantiatePreviousMethodName = "instantiatePrevious"; final Config config; ClassGenerator(this.config); int get nbClasses => config.nbClasses; int get nbMethodsPerClass => config.nbMethodsPerClass; bool get shouldPrintInMethod => config.shouldPrintInMethod; bool get sameMethodNames => config.sameMethodNames; int get nbWhileLoopsInMethod => config.nbWhileLoopsInBody; bool get shareCommonSuperclass => config.shareCommonSuperclass; bool get shouldEmitCallAllMethods => config.shouldEmitCallAllMethods; bool get shouldEmitInstantiatePreviousMethod => config.shouldEmitInstantiatePreviousMethod; int get fakeInstantiateClass => config.fakeInstantiateClass; int get instantiateClassesPercent => config.instantiateClassesPercent; Future measure(String filePrefix) async { String fileName = await generateRawJs(filePrefix); if (fileName == null) return; Directory dir = Directory.systemTemp.createTempSync('classes'); try { File measuring = new File("${dir.path}/measuring.js"); IOSink sink = measuring.openWrite(); sink.writeln("var start = new Date();"); await sink.addStream(new File(fileName).openRead().cast>()); sink.writeln("print(new Date() - start)"); String command; List args; bool runJsShell = false; if (runJsShell) { command = jsShellPath; print("Running $command"); args = [measuring.path]; } else { command = d8Path; print("Running $command"); args = ["--harmony-sloppy", measuring.path]; } print("Running: $fileName"); int nbRuns = 10; int sum = 0; int sumSw = 0; Stopwatch watch = new Stopwatch(); for (int i = 0; i < nbRuns; i++) { watch.reset(); watch.start(); ProcessResult result = await Process.run(command, args); if (result.exitCode != 0) { print("run failed"); print(result.stdout); print(result.stderr); } int elapsed = watch.elapsedMilliseconds; print(" output: ${result.stdout.trim()} ($elapsed)"); sum += int.parse(result.stdout, onError: (str) => 0); sumSw += elapsed; } int mean = sum == 0 ? 0 : sum ~/ nbRuns; int meanSw = sumSw == 0 ? 0 : sumSw ~/ nbRuns; print(" mean: $mean ($meanSw)"); } finally { dir.deleteSync(recursive: true); } } Future generateRawJs(String filePrefix); String buildFileName(String filePrefix, String extension) { // TODO(floitsch): store other config info in the file name. return "$filePrefix.$nbClasses.$nbMethodsPerClass." "$instantiateClassesPercent.$description.$extension"; } String writeFile(String filePrefix) { buffer.clear(); emitClasses(); // Output is stored in `buffer`. String fileName = buildFileName(filePrefix, fileExtension); new File(fileName).writeAsStringSync(buffer.toString()); print("wrote: $fileName"); return fileName; } void writeln(x) => buffer.writeln(x); String classIdToName(int id) { if (id < 0) id = nbClasses + id; return "Class$id"; } /// [id] is per class. String methodIdToName(int id, int classId) { if (sameMethodNames) return "method$id"; return "method${classId}_$id"; } // Must work for Dart and JS. void emitMethodBody(int methodId, int classId) { writeln("{"); if (shouldPrintInMethod) { writeln("print('class: $classId, method: $methodId');"); } if (nbWhileLoopsInMethod > 0) { writeln("var sum = 0;"); for (int i = 0; i < nbWhileLoopsInMethod; i++) { writeln("for (var i = 0; i < $argumentName; i++) {"); writeln(" sum++;"); writeln("}"); } writeln("return sum;"); } writeln("}"); } // Must work for Dart and JS. void emitCallOtherMethodsBody(List methodIds, int classId) { writeln("{"); writeln("var sum = 0;"); for (int methodId in methodIds) { String methodName = methodIdToName(methodId, classId); writeln("sum += this.$methodName($argumentName);"); } writeln("return sum;"); writeln("}"); } // Must work for Dart and JS. void emitInstantiatePrevious(int classId) { writeln("{"); if (classId == 0) { writeln("return null;"); } else { String previousClass = classIdToName(classId - 1); writeln("return new $previousClass();"); } writeln("}"); } /// Should write the class using [writeln]. void emitClasses(); String get description; String get fileExtension; } abstract class JavaScriptClassGenerator extends ClassGenerator { bool get wrapProgram => config.shouldWrapProgram; final String methodsObjectName = "methods"; JavaScriptClassGenerator(Config config) : super(config); Future generateRawJs(String filePrefix) => new Future.value(writeFile(filePrefix)); void emitUtilityFunctions(); void emitClass(int classId, int superclassId); void emitClasses() { if (wrapProgram) writeln("(function() {"); writeln("var $methodsObjectName;"); emitUtilityFunctions(); for (int i = 0; i < nbClasses; i++) { int superclassId = shareCommonSuperclass && i != 0 ? 0 : null; emitClass(i, superclassId); } if (fakeInstantiateClass != null) { String className = classIdToName(fakeInstantiateClass); writeln(""" if (new Date() == 42) { var o = new $className(); do { o.$callOtherMethodsName(99); o = o.$instantiatePreviousMethodName(); } while(o != null); }"""); } if (instantiateClassesPercent != null) { int targetClassId = ((nbClasses - 1) * instantiateClassesPercent) ~/ 100; String targetClassName = classIdToName(targetClassId); writeln(""" var o = new $targetClassName(); do { o = o.$instantiatePreviousMethodName(); } while(o != null); """); } if (wrapProgram) writeln("})();"); } String get fileExtension => "js"; } enum PrototypeApproach { tmpFunction, internalProto, objectCreate } class PlainJavaScriptClassGenerator extends JavaScriptClassGenerator { final PrototypeApproach prototypeApproach; final bool shouldInlineInherit; final bool useMethodsObject; PlainJavaScriptClassGenerator(Config config, {this.prototypeApproach, this.shouldInlineInherit, this.useMethodsObject}) : super(config) { if (prototypeApproach == null) { throw "Must provide prototype approach"; } if (shouldInlineInherit == null) { throw "Must provide inlining approach"; } if (useMethodsObject == null) { throw "Must provide object-proto approach"; } if (shouldInlineInherit && prototypeApproach == PrototypeApproach.tmpFunction) { throw "Can't inline tmp-function approach"; } } void emitInherit(cls, superclassId) { if (superclassId == null && !useMethodsObject) return; String sup = (superclassId == null) ? "null" : classIdToName(superclassId); if (!shouldInlineInherit) { if (useMethodsObject) { writeln("inherit($cls, $sup, $methodsObjectName);"); } else { writeln("inherit($cls, $sup);"); } return; } switch (prototypeApproach) { case PrototypeApproach.tmpFunction: throw "Should not happen"; break; case PrototypeApproach.internalProto: if (useMethodsObject) { writeln("$cls.prototype = $methodsObjectName;"); } if (superclassId != null) { writeln("$cls.prototype.__proto__ = $sup.prototype;"); } break; case PrototypeApproach.objectCreate: if (useMethodsObject) { if (superclassId == null) { writeln("$cls.prototype = $methodsObjectName;"); } else { writeln("$cls.prototype = Object.create($sup.prototype);"); writeln("copyProperties($methodsObjectName, $cls.prototype);"); } } else { writeln("$cls.prototype = Object.create($sup.prototype);"); } break; } } void emitUtilityFunctions() { switch (prototypeApproach) { case PrototypeApproach.internalProto: if (useMethodsObject) { writeln(''' function inherit(cls, sup, methods) { cls.prototype = methods; if (sup != null) { cls.prototype.__proto__ = sup.prototype; } } '''); } else { writeln(''' function inherit(cls, sup) { cls.prototype.__proto__ = sup.prototype; } '''); } break; case PrototypeApproach.tmpFunction: if (useMethodsObject) { writeln(''' function inherit(cls, sup, methods) { if (sup != null) { function tmp() {} tmp.prototype = sup.prototype; var proto = new tmp(); proto.constructor = cls; cls.prototype = proto; } copyProperties(methods, cls.prototype); }'''); } else { writeln(''' function inherit(cls, sup) { function tmp() {} tmp.prototype = sup.prototype; var proto = new tmp(); proto.constructor = cls; cls.prototype = proto; }'''); } break; case PrototypeApproach.objectCreate: if (useMethodsObject) { writeln(''' function inherit(cls, sup, methods) { if (sup == null) { cls.prototype = methods; } else { cls.prototype = Object.create(sup.prototype); copyProperties(methods, cls.prototype); } } '''); } else { writeln(''' function inherit(cls, sup) { cls.prototype = Object.create(sup.prototype); } '''); } break; } writeln(""" function copyProperties(from, to) { var props = Object.keys(from); for (var i = 0; i < props.length; i++) { var p = props[i]; to[p] = from[p]; } }"""); } void emitMethod(int classId, String methodName, Function bodyEmitter, {bool emitArgument: true}) { String argumentString = emitArgument ? argumentName : ""; if (useMethodsObject) { writeln("$methodName: function($argumentString)"); bodyEmitter(); writeln(","); } else { String className = classIdToName(classId); String proto = "$className.prototype"; writeln("$proto.$methodName = function($argumentString)"); bodyEmitter(); } } /// Returns the methods object, if we use an object. void emitMethods(int classId) { List methodIds = []; int nbGenericMethods = nbMethodsPerClass; if (useMethodsObject) { writeln("$methodsObjectName = {"); } if (shouldEmitCallAllMethods) nbGenericMethods--; for (int j = 0; j < nbGenericMethods; j++) { String methodName = methodIdToName(j, classId); emitMethod(classId, methodName, () => emitMethodBody(j, classId)); methodIds.add(j); } if (shouldEmitCallAllMethods) { emitMethod(classId, callOtherMethodsName, () => emitCallOtherMethodsBody(methodIds, classId)); } if (shouldEmitInstantiatePreviousMethod) { emitMethod(classId, instantiatePreviousMethodName, () => emitInstantiatePrevious(classId), emitArgument: false); } if (useMethodsObject) { writeln("};"); } } void emitClass(int classId, int superclassId) { String className = classIdToName(classId); writeln("function $className() {}"); switch (prototypeApproach) { case PrototypeApproach.objectCreate: if (useMethodsObject) { emitMethods(classId); emitInherit(className, superclassId); } else { emitInherit(className, superclassId); emitMethods(classId); } break; case PrototypeApproach.tmpFunction: if (useMethodsObject) { emitMethods(classId); emitInherit(className, superclassId); } else { emitInherit(className, superclassId); emitMethods(classId); } break; case PrototypeApproach.internalProto: emitMethods(classId); emitInherit(className, superclassId); break; } } String get description { String protoApproachDescription; switch (prototypeApproach) { case PrototypeApproach.objectCreate: protoApproachDescription = "objectCreate"; break; case PrototypeApproach.tmpFunction: protoApproachDescription = "tmpFunction"; break; case PrototypeApproach.internalProto: protoApproachDescription = "internalProto"; break; } String inline = shouldInlineInherit ? "inl" : "noInl"; String objectProto = useMethodsObject ? "obj" : "noObj"; return "plain_${protoApproachDescription}_${inline}_$objectProto"; } } class Es6ClassGenerator extends JavaScriptClassGenerator { Es6ClassGenerator(Config config) : super(config); void emitUtilityFunctions() {} void emitClass(int classId, int superclassId) { String className = classIdToName(classId); if (superclassId != null) { String superclassName = classIdToName(superclassId); buffer.writeln("class $className extends $superclassName {"); } else { buffer.writeln("class $className {"); } List methodIds = []; int nbGenericMethods = nbMethodsPerClass; if (shouldEmitCallAllMethods) nbGenericMethods--; for (int j = 0; j < nbGenericMethods; j++) { String methodName = methodIdToName(j, classId); writeln("$methodName($argumentName) "); emitMethodBody(j, classId); methodIds.add(j); } if (shouldEmitCallAllMethods) { writeln("$callOtherMethodsName($argumentName)"); emitCallOtherMethodsBody(methodIds, classId); } if (shouldEmitInstantiatePreviousMethod) { writeln("$instantiatePreviousMethodName()"); emitInstantiatePrevious(classId); } writeln("}"); } String get description => "es6"; } class DartClassGenerator extends ClassGenerator { final bool shouldUseNewEmitter; DartClassGenerator(Config config, {this.shouldUseNewEmitter: false}) : super(config); void emitClasses() { // TODO(flo): instantiateAndCallPrevious for (int i = 0; i < nbClasses; i++) { String className = classIdToName(i); writeln("class $className {"); List methodIds = []; int nbGenericMethods = nbMethodsPerClass; if (shouldEmitCallAllMethods) nbGenericMethods--; for (int j = 0; j < nbGenericMethods; j++) { String methodName = methodIdToName(j, i); writeln("$methodName($argumentName)"); emitMethodBody(j, i); methodIds.add(j); } if (shouldEmitCallAllMethods) { writeln("$callOtherMethodsName($argumentName)"); emitCallOtherMethodsBody(methodIds, i); } if (shouldEmitInstantiatePreviousMethod) { writeln("$instantiatePreviousMethodName()"); emitInstantiatePrevious(i); } writeln("}"); } writeln("main() {"); if (fakeInstantiateClass != null) { String className = classIdToName(fakeInstantiateClass); writeln(""" if (new DateTime.now().millisecondsSinceEpoch == 42) { var o = new $className(); do { o.$callOtherMethodsName(99); o = o.$instantiatePreviousMethodName(); } while(o != null); }"""); } if (instantiateClassesPercent != null) { int targetClassId = ((nbClasses - 1) * instantiateClassesPercent) ~/ 100; String targetClassName = classIdToName(targetClassId); writeln(""" var o = new $targetClassName(); do { o = o.$instantiatePreviousMethodName(); } while(o != null); """); } writeln("}"); } Future generateRawJs(String filePrefix) async { String dartFile = writeFile(filePrefix); String outFile = buildFileName(filePrefix, "js"); Map env = {}; if (shouldUseNewEmitter) { env["DART_VM_OPTIONS"] = '-Ddart2js.use.new.emitter=true'; } print("compiling"); print("dart2jsPath: $dart2jsPath"); ProcessResult result = await Process .run(dart2jsPath, [dartFile, "--out=$outFile"], environment: env); if (result.exitCode != 0) { print("compilation failed"); print(result.stdout); print(result.stderr); return null; } print("compilation done"); return outFile; } Future measureDart(String filePrefix, {bool useSnapshot: false}) async { String dartFile = writeFile(filePrefix); String command = Platform.executable; Stopwatch watch = new Stopwatch(); Directory dir = Directory.systemTemp.createTempSync('snapshot'); try { String measuring = dartFile; if (useSnapshot) { print("creating snapshot"); measuring = new File("${dir.path}/measuring.snapshot").path; ProcessResult result = await Process.run(command, ["--snapshot=$measuring", dartFile]); if (result.exitCode != 0) { print("snapshot creation failed"); print(result.stdout); print(result.stderr); return; } } List args = [measuring]; print("Running: $command ${args.join(' ')}"); int nbRuns = 10; int sum = 0; for (int i = 0; i < nbRuns; i++) { watch.reset(); watch.start(); ProcessResult result = await Process.run(command, args); int elapsedMilliseconds = watch.elapsedMilliseconds; if (result.exitCode != 0) { print("run failed"); print(result.stdout); print(result.stderr); return; } print(" measured time (including VM startup): $elapsedMilliseconds"); sum += elapsedMilliseconds; } if (sum != 0) { print(" mean: ${sum ~/ nbRuns}"); } } finally { dir.deleteSync(recursive: true); } } String get fileExtension => "dart"; String get description { if (shouldUseNewEmitter) return "dartNew"; return "dart"; } } main(List arguments) async { String filePrefix = arguments.length > 0 ? arguments.first : Directory.systemTemp.uri.resolve("classes").path; Config config = new Config( nbClasses: 2000, nbMethodsPerClass: 20, fakeInstantiateClass: -1, instantiateClassesPercent: 20, shareCommonSuperclass: true, sameMethodNames: true, shouldPrintInMethod: true, nbWhileLoopsInBody: 1, shouldWrapProgram: true, shouldEmitCallAllMethods: true, shouldEmitInstantiatePreviousMethod: true); var plain = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.tmpFunction, useMethodsObject: false, shouldInlineInherit: false); var plainProto = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.internalProto, useMethodsObject: false, shouldInlineInherit: false); var plainObjectCreate = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.objectCreate, useMethodsObject: false, shouldInlineInherit: false); var plainProtoInline = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.internalProto, useMethodsObject: false, shouldInlineInherit: true); var plainObjectCreateInline = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.objectCreate, useMethodsObject: false, shouldInlineInherit: true); var plainObj = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.tmpFunction, useMethodsObject: true, shouldInlineInherit: false); var plainProtoObj = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.internalProto, useMethodsObject: true, shouldInlineInherit: false); var plainObjectCreateObj = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.objectCreate, useMethodsObject: true, shouldInlineInherit: false); var plainProtoInlineObj = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.internalProto, useMethodsObject: true, shouldInlineInherit: true); var plainObjectCreateInlineObj = new PlainJavaScriptClassGenerator(config, prototypeApproach: PrototypeApproach.objectCreate, useMethodsObject: true, shouldInlineInherit: true); var es6 = new Es6ClassGenerator(config); var dart = new DartClassGenerator(config); var dartNew = new DartClassGenerator(config, shouldUseNewEmitter: true); await plain.measure(filePrefix); await plainProto.measure(filePrefix); await plainObjectCreate.measure(filePrefix); await plainProtoInline.measure(filePrefix); await plainObjectCreateInline.measure(filePrefix); await plainObj.measure(filePrefix); await plainProtoObj.measure(filePrefix); await plainObjectCreateObj.measure(filePrefix); await plainProtoInlineObj.measure(filePrefix); await plainObjectCreateInlineObj.measure(filePrefix); await es6.measure(filePrefix); await dartNew.measure(filePrefix); await dart.measure(filePrefix); await dart.measureDart(filePrefix); await dart.measureDart(filePrefix, useSnapshot: true); }