Browse Source

Add Dart Angel Framework (#4013)

* create angel

* Angel project setup

* bin/main.dart

* Spawn $(N_CORES) instances

* Pass args to Angel entrypoint

* Move newline in usage print

* Created model files

* Include serializers as parts

* Connect to Mongo

* Add JSON+plaintext

* Fetch random world

* Fortunes + remaining routes

* Add mustache

* Postgres

* Run pub get instead of slower pub upgrade

* Add missing URLS

* Commit lockfile

* Update world.dart to serialize as randomNumber instead of randomnumber

* ignore .g.part

* Remove .g.part

* All tests run for Angel

* Angel postgres passes

* Remove development config

* Remove logging config

* Version information for Angel benchmarks

* Add link to Dart benchmarks

* Format Dart code

* Remove logging usage in bin/main.dart

* Remove Dart gen files, run them in Docker

* ignore

* angel
Tobe Osakwe 7 years ago
parent
commit
f2f03f14f0

+ 5 - 0
frameworks/Dart/angel/.gitignore

@@ -0,0 +1,5 @@
+!bin/
+.dart_tool
+.packages
+*.g.part
+*.g.dart

+ 39 - 0
frameworks/Dart/angel/README.md

@@ -0,0 +1,39 @@
+# Angel Framework Benchmarking Test
+This test adds [Angel](https://angel-dart.github.io),
+a full-featured framework for Dart, to the
+[benchmarking test suite](https://github.com/TechEmpower/FrameworkBenchmarks/blob/master/frameworks/Dart). The test is based on the Dart Benchmarking Test.
+
+## Versions
+The `pubspec.lock` file is included; so that dependencies are kept consistent between deployments.
+The tests included in this benchmark are a demonstration of:
+* [Dart SDK version 2.0.0](http://www.dartlang.org/)
+* [Angel Framework version `^2.0.0-alpha`](https://pub.dartlang.org/packages/angel_framework/versions/2.0.0-alpha.1)
+
+## Test URLs
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/query?queries=
+
+### CACHED QUERY
+
+http://localhost:8080/cached_query?queries=
+
+### UPDATE
+
+http://localhost:8080/update?queries=
+
+### FORTUNES
+
+http://localhost:8080/fortunes

+ 9 - 0
frameworks/Dart/angel/angel-postgres.dockerfile

@@ -0,0 +1,9 @@
+FROM google/dart:2.0
+
+COPY ./ ./
+
+RUN pub get
+
+RUN pub run build_runner build
+
+CMD ANGEL_ENV=production dart bin/main.dart --type=postgres

+ 9 - 0
frameworks/Dart/angel/angel.dockerfile

@@ -0,0 +1,9 @@
+FROM google/dart:2.0
+
+COPY ./ ./
+
+RUN pub get
+
+RUN pub run build_runner build
+
+CMD ANGEL_ENV=production dart bin/main.dart --type=mongo

+ 53 - 0
frameworks/Dart/angel/benchmark_config.json

@@ -0,0 +1,53 @@
+{
+  "framework": "angel",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries/?queryCount=",
+        "update_url": "/updates/?queryCount=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Fullstack",
+        "database": "mongodb",
+        "framework": "angel-mongo",
+        "language": "Dart",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Angel (MongoDB)",
+        "notes": "",
+        "versus": "dart"
+      },
+      "postgres": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries/?queryCount=",
+        "update_url": "/updates/?queryCount=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Fullstack",
+        "database": "postgres",
+        "framework": "angel-postgres",
+        "language": "Dart",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Angel (MongoDB)",
+        "notes": "",
+        "versus": "dart"
+      }
+    }
+  ]
+}

+ 69 - 0
frameworks/Dart/angel/bin/main.dart

@@ -0,0 +1,69 @@
+import 'dart:async';
+import 'dart:io';
+import 'dart:isolate';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:args/args.dart';
+import 'package:dart_angel_benchmark/dart_angel_benchmark.dart'
+    as dart_angel_benchmark;
+
+main(List<String> args) async {
+  var argParser = ArgParser()
+    ..addOption('type',
+        abbr: 't', allowed: ['mongo', 'postgres'], defaultsTo: 'mongo');
+
+  try {
+    var argResults = argParser.parse(args);
+    serverMain(StartConfig(0, argResults));
+
+    for (int i = 1; i < Platform.numberOfProcessors; i++) {
+      var onError = new ReceivePort();
+      onError.first.then((data) {
+        print(data);
+
+        if (data is List) {
+          Zone.current.errorCallback(data[0], data[1] as StackTrace);
+        }
+      });
+      Isolate.spawn(serverMain, StartConfig(i, argResults),
+          onError: onError.sendPort);
+    }
+  } on ArgParserException catch (e) {
+    stderr
+      ..writeln('fatal error: ${e.message}')
+      ..writeln('usage: bin/main.dart [options...]')
+      ..writeln()
+      ..writeln(argParser.usage);
+    exitCode = 1;
+  }
+}
+
+void serverMain(StartConfig config) {
+  var app = Angel(
+      //logger: Logger('tfb'),
+      );
+
+  // hierarchicalLoggingEnabled = true;
+
+  //app.logger.onRecord.listen((rec) {
+  //  print(rec);
+  //  if (rec.error != null) print(rec.error);
+  //  if (rec.stackTrace != null) print(rec.stackTrace);
+  //});
+
+  app
+      .configure(dart_angel_benchmark.configureServer(config.argResults))
+      .then((_) async {
+    var http = AngelHttp.custom(app, startShared);
+    var server = await http.startServer('0.0.0.0', 8080);
+    var url =
+        Uri(scheme: 'http', host: server.address.address, port: server.port);
+    print('Instance #${config.id} listening at $url');
+  });
+}
+
+class StartConfig {
+  final int id;
+  final ArgResults argResults;
+
+  StartConfig(this.id, this.argResults);
+}

+ 6 - 0
frameworks/Dart/angel/config/default.yaml

@@ -0,0 +1,6 @@
+postgres:
+  username: benchmarkdbuser
+  password: benchmarkdbpass
+  host: tfb-database
+  port: 5432
+  databaseName: hello_world

+ 1 - 0
frameworks/Dart/angel/config/production.yaml

@@ -0,0 +1 @@
+mongo_db: "mongodb://tfb-database/hello_world"

+ 138 - 0
frameworks/Dart/angel/lib/dart_angel_benchmark.dart

@@ -0,0 +1,138 @@
+import 'dart:async';
+import 'dart:io';
+import 'dart:math';
+import 'package:angel_configuration/angel_configuration.dart';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:args/args.dart';
+import 'package:file/local.dart';
+import 'package:http_parser/http_parser.dart';
+import 'package:mongo_dart/mongo_dart.dart';
+import 'package:mustache4dart/mustache4dart.dart' as mustache;
+import 'package:postgres/postgres.dart';
+import 'src/models/models.dart';
+import 'src/query/query.dart';
+
+AngelConfigurer configureServer(ArgResults argResults) {
+  var rnd = new Random();
+  var minQueryCount = 1;
+  var maxQueryCount = 500;
+  var worldTableSize = 10000;
+  var fs = const LocalFileSystem();
+
+  return (Angel app) async {
+    // Load configuration.
+    await app.configure(configuration(fs));
+
+    // Set up the view engine.
+    var fortunesTemplate =
+        await fs.file('views/fortunes.mustache').readAsString();
+
+    app.viewGenerator =
+        (name, [data]) => mustache.render(fortunesTemplate, data);
+
+    // Select a querier, either MongoDB or PostgreSQL.
+    //
+    // Either way, the container *must* contain a `Querier`.
+    if (argResults['type'] == 'mongo') {
+      var db = Db(app.configuration['mongo_db']);
+      app.container
+          .registerSingleton<Querier>(MongoQuerier(db, rnd, worldTableSize));
+      await db.open();
+      app.shutdownHooks.add((_) => db.close());
+    } else if (argResults['type'] == 'postgres') {
+      var postgresConfig = app.configuration['postgres'] as Map;
+      var connection = PostgreSQLConnection(
+        postgresConfig['host'],
+        postgresConfig['port'],
+        postgresConfig['databaseName'],
+        username: postgresConfig['username'],
+        password: postgresConfig['password'],
+      );
+      app.container.registerSingleton<Querier>(PostgresQuerier(connection));
+      await connection.open();
+      app.shutdownHooks.add((_) => connection.close());
+    } else {
+      throw UnsupportedError('Unsupported DB ${argResults['type']}');
+    }
+
+    // Always add a Date header.
+    app.fallback((req, res) {
+      res.headers['date'] = HttpDate.format(DateTime.now());
+    });
+
+    // JSON response.
+    app.get('/json', (req, res) {
+      res.serialize({'message': 'Hello, World!'});
+    });
+
+    // Plaintext response.
+    app.get('/plaintext', (req, res) {
+      res
+        ..write('Hello, World!')
+        ..close();
+    });
+
+    // Fetch random world object.
+    app.get('/db', (req, res) async {
+      var querier = req.container.make<Querier>();
+      res.serialize(await querier.getRandomWorld());
+    });
+
+    // DB queries
+    app.get('/queries', (req, res) async {
+      // Get the querier and query count.
+      var querier = req.container.make<Querier>();
+      var queryCount =
+          int.tryParse(req.uri.queryParameters['queryCount'].toString()) ??
+              minQueryCount;
+      queryCount = queryCount.clamp(minQueryCount, maxQueryCount);
+
+      // Fetch the objects.
+      var worlds = await Future.wait<World>(
+          List.generate(queryCount, (_) => querier.getRandomWorld()));
+      res.serialize(worlds);
+    });
+
+    // DB updates
+    app.get('/updates', (req, res) async {
+      // Get the querier and query count.
+      var querier = req.container.make<Querier>();
+      var queryCount =
+          int.tryParse(req.uri.queryParameters['queryCount'].toString()) ??
+              minQueryCount;
+      queryCount = queryCount.clamp(minQueryCount, maxQueryCount);
+
+      // Fetch the objects.
+      var worlds =
+          await Future.wait<World>(List.generate(queryCount, (_) async {
+        var world = await querier.getRandomWorld();
+        world = world.copyWith(randomNumber: rnd.nextInt(worldTableSize) + 1);
+        await querier.updateWorld(world.id, world);
+        return world;
+      }));
+      res.serialize(worlds);
+    });
+
+    // Templating
+    app.get('/fortunes', (req, res) async {
+      var querier = req.container.make<Querier>();
+      var fortunes = await querier.getFortunes();
+
+      // Insert an additional fortune.
+      fortunes.add(
+        Fortune(
+          id: 0,
+          message: 'Additional fortune added at request time.',
+        ),
+      );
+
+      // Sort the fortunes.
+      fortunes.sort((a, b) => a.message.compareTo(b.message));
+
+      // Render the template.
+      res.contentType = new MediaType('text', 'html', {'charset': 'utf-8'});
+      await res.render('fortunes',
+          {'fortunes': fortunes.map((f) => f.copyWith(id: f.id.toInt()))});
+    });
+  };
+}

+ 10 - 0
frameworks/Dart/angel/lib/src/models/fortune.dart

@@ -0,0 +1,10 @@
+import 'package:angel_serialize/angel_serialize.dart';
+part 'fortune.g.dart';
+part 'fortune.serializer.g.dart';
+
+@Serializable(autoIdAndDateFields: false, autoSnakeCaseNames: false)
+abstract class _Fortune {
+  num get id;
+
+  String get message;
+}

+ 2 - 0
frameworks/Dart/angel/lib/src/models/models.dart

@@ -0,0 +1,2 @@
+export 'fortune.dart';
+export 'world.dart';

+ 10 - 0
frameworks/Dart/angel/lib/src/models/world.dart

@@ -0,0 +1,10 @@
+import 'package:angel_serialize/angel_serialize.dart';
+part 'world.g.dart';
+part 'world.serializer.g.dart';
+
+@Serializable(autoIdAndDateFields: false, autoSnakeCaseNames: false)
+abstract class _World {
+  num get id;
+
+  num get randomNumber;
+}

+ 37 - 0
frameworks/Dart/angel/lib/src/query/mongo.dart

@@ -0,0 +1,37 @@
+import 'dart:async';
+import 'dart:math';
+import 'package:dart_angel_benchmark/src/models/fortune.dart';
+import 'package:mongo_dart/mongo_dart.dart';
+import '../models/models.dart';
+import 'querier.dart';
+
+class MongoQuerier implements Querier {
+  final Db db;
+  final Random rnd;
+  final int worldTableSize;
+  DbCollection _fortunes, _worlds;
+
+  MongoQuerier(this.db, this.rnd, this.worldTableSize) {
+    _fortunes = db.collection('fortune');
+    _worlds = db.collection('world');
+  }
+
+  @override
+  Future<List<Fortune>> getFortunes() {
+    return _fortunes.find().map(FortuneSerializer.fromMap).toList();
+  }
+
+  @override
+  Future<World> getRandomWorld() {
+    return _worlds
+        .findOne(where.skip(rnd.nextInt(worldTableSize)))
+        .then(WorldSerializer.fromMap);
+  }
+
+  @override
+  Future<World> updateWorld(id, World world) {
+    return _worlds
+        .update(where.eq('id', id), world.toJson())
+        .then(WorldSerializer.fromMap);
+  }
+}

+ 41 - 0
frameworks/Dart/angel/lib/src/query/postgres.dart

@@ -0,0 +1,41 @@
+import 'dart:async';
+import 'package:dart_angel_benchmark/src/models/fortune.dart';
+import 'package:postgres/postgres.dart';
+import '../models/models.dart';
+import 'querier.dart';
+
+class PostgresQuerier implements Querier {
+  final PostgreSQLConnection connection;
+
+  PostgresQuerier(this.connection);
+
+  static Fortune parseFortune(List row) {
+    return Fortune(id: row[0], message: row[1]);
+  }
+
+  static World parseWorld(List row) {
+    return World(id: row[0], randomNumber: row[1]);
+  }
+
+  @override
+  Future<List<Fortune>> getFortunes() {
+    return connection.query('SELECT id, message FROM fortune').then((rows) {
+      return rows.map((parseFortune)).toList();
+    });
+  }
+
+  @override
+  Future<World> getRandomWorld() async {
+    var rows = await connection
+        .query('SELECT id, randomNumber FROM world ORDER BY RANDOM() LIMIT 1');
+    return parseWorld(rows[0]);
+  }
+
+  @override
+  Future<World> updateWorld(id, World world) async {
+    await connection.query(
+        'UPDATE world SET randomNumber = @randomNumber WHERE id = @id',
+        substitutionValues: {'id': id, 'randomNumber': world.randomNumber});
+    return world;
+  }
+}

+ 10 - 0
frameworks/Dart/angel/lib/src/query/querier.dart

@@ -0,0 +1,10 @@
+import 'dart:async';
+import '../models/models.dart';
+
+abstract class Querier {
+  Future<List<Fortune>> getFortunes();
+
+  Future<World> getRandomWorld();
+
+  Future<World> updateWorld(id, World world);
+}

+ 3 - 0
frameworks/Dart/angel/lib/src/query/query.dart

@@ -0,0 +1,3 @@
+export 'mongo.dart';
+export 'postgres.dart';
+export 'querier.dart';

+ 16 - 0
frameworks/Dart/angel/pubspec.yaml

@@ -0,0 +1,16 @@
+name: dart_angel_benchmark
+publish_to: none
+environment:
+  sdk: ">=2.0.0-dev <3.0.0"
+dependencies:
+  angel_configuration: ^2.0.0
+  angel_framework: ^2.0.0-alpha
+  angel_model: ^1.0.0
+  angel_serialize: ^2.0.0
+  args: ^1.0.0
+  mongo_dart: ^0.3.0
+  mustache4dart: ^3.0.0-dev.0.0
+  postgres: ^1.0.0
+dev_dependencies:
+  angel_serialize_generator: ^2.0.0
+  build_runner: ^0.10.0

+ 1 - 0
frameworks/Dart/angel/views/fortunes.mustache

@@ -0,0 +1 @@
+<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>{{#fortunes}}<tr><td>{{id}}</td><td>{{message}}</td></tr>{{/fortunes}}</table></body></html>