server.dart 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import 'dart:async' show Future;
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'dart:isolate';
  5. import 'dart:math' show Random;
  6. import 'package:args/args.dart' show ArgParser;
  7. import 'package:mustache/mustache.dart' as mustache;
  8. import 'package:postgresql/postgresql.dart' as pg;
  9. import 'package:postgresql/postgresql_pool.dart' as pgpool;
  10. import 'package:yaml/yaml.dart' as yaml;
  11. final _encoder = new JsonUtf8Encoder();
  12. /// Starts a new HTTP server that implements the tests to be benchmarked. The
  13. /// address and port for incoming connections is configurable via command line
  14. /// arguments, as is the number of database connections to be maintained in the
  15. /// connection pool.
  16. void main(List<String> args) {
  17. var parser = new ArgParser()
  18. ..addOption('address', abbr: 'a', defaultsTo: '0.0.0.0')
  19. ..addOption('port', abbr: 'p', defaultsTo: '8080')
  20. ..addOption('dbconnections', abbr: 'd', defaultsTo: '256')
  21. ..addOption('isolates', abbr: 'i', defaultsTo: '1');
  22. var arguments = parser.parse(args);
  23. var isolates = int.parse(arguments['isolates']);
  24. var dbConnections = int.parse(arguments['dbconnections']) ~/ isolates;
  25. ServerSocket
  26. .bind(arguments['address'], int.parse(arguments['port']))
  27. .then((server) {
  28. var ref = server.reference;
  29. for (int i = 1; i < isolates; i++) {
  30. Isolate.spawn(startInIsolate, [ref, dbConnections]);
  31. }
  32. _startServer(server, dbConnections);
  33. });
  34. }
  35. void startInIsolate(List args) {
  36. var ref = args[0];
  37. var dbConnections = args[1];
  38. ref.create().then((server) {
  39. _startServer(server, dbConnections);
  40. });
  41. }
  42. /// The entity used in the database query and update tests.
  43. class World {
  44. int id;
  45. int randomnumber;
  46. World(this.id, this.randomnumber);
  47. toJson() => {'id': id, 'randomNumber': randomnumber};
  48. }
  49. /// The entity used in the fortunes test.
  50. class Fortune implements Comparable<Fortune> {
  51. int id;
  52. String message;
  53. Fortune(this.id, this.message);
  54. int compareTo(Fortune other) => message.compareTo(other.message);
  55. }
  56. /// The number of rows in the world entity table.
  57. const _WORLD_TABLE_SIZE = 10000;
  58. /// A random number generator.
  59. final _RANDOM = new Random();
  60. /// The PostgreSQL connection pool used by all the tests that require database
  61. /// connectivity.
  62. pgpool.Pool _connectionPool;
  63. /// The mustache template which is rendered in the fortunes test.
  64. mustache.Template _fortunesTemplate;
  65. /// Starts a benchmark server, which listens for connections from
  66. /// '[address] : [port]' and maintains [dbConnections] connections to the
  67. /// database.
  68. void _startServer(serverSocket, dbConnections) {
  69. Future.wait([
  70. new File('postgresql.yaml').readAsString().then((config) {
  71. _connectionPool = new pgpool.Pool(
  72. new pg.Settings.fromMap(yaml.loadYaml(config)).toUri(),
  73. min: dbConnections, max: dbConnections);
  74. return _connectionPool.start();
  75. }),
  76. new File('fortunes.mustache').readAsString().then((template) {
  77. _fortunesTemplate = mustache.parse(template);
  78. })
  79. ]).then((_) {
  80. var server = new HttpServer.listenOn(serverSocket);
  81. server.defaultResponseHeaders.clear();
  82. server.serverHeader = 'dart';
  83. server.listen((request) {
  84. switch (request.uri.path) {
  85. case '/json':
  86. _jsonTest(request);
  87. break;
  88. case '/db':
  89. _dbTest(request);
  90. break;
  91. case '/queries':
  92. _queriesTest(request);
  93. break;
  94. case '/fortunes':
  95. _fortunesTest(request);
  96. break;
  97. case '/updates':
  98. _updatesTest(request);
  99. break;
  100. case '/plaintext':
  101. _plaintextTest(request);
  102. break;
  103. default:
  104. _sendResponse(request, HttpStatus.NOT_FOUND);
  105. break;
  106. }
  107. });
  108. });
  109. }
  110. /// Returns the given [text] parsed as a base 10 integer. If the text is null
  111. /// or is an otherwise invalid representation of a base 10 integer, zero is
  112. /// returned.
  113. int _parseInt(String text) =>
  114. (text == null) ? 0 : int.parse(text, radix: 10, onError: ((_) => 0));
  115. /// Completes the given [request] by writing the [response] with the given
  116. /// [statusCode] and [type].
  117. void _sendResponse(HttpRequest request, int statusCode,
  118. {ContentType type, List<int> response}) {
  119. request.response.statusCode = statusCode;
  120. request.response.headers.date = new DateTime.now();
  121. if (type != null) {
  122. request.response.headers.contentType = type;
  123. }
  124. if (response != null) {
  125. request.response.contentLength = response.length;
  126. request.response.add(response);
  127. } else {
  128. request.response.contentLength = 0;
  129. }
  130. request.response.close();
  131. }
  132. /// Completes the given [request] by writing the [response] as HTML.
  133. void _sendHtml(HttpRequest request, String response) {
  134. _sendResponse(request, HttpStatus.OK,
  135. type: ContentType.HTML, response: UTF8.encode(response));
  136. }
  137. /// Completes the given [request] by writing the [response] as JSON.
  138. void _sendJson(HttpRequest request, Object response) {
  139. _sendResponse(request, HttpStatus.OK,
  140. type: ContentType.JSON, response: _encoder.convert(response));
  141. }
  142. /// Completes the given [request] by writing the [response] as plain text.
  143. void _sendText(HttpRequest request, String response) {
  144. _sendResponse(request, HttpStatus.OK,
  145. type: ContentType.TEXT, response: UTF8.encode(response));
  146. }
  147. /// Responds with the JSON test to the [request].
  148. void _jsonTest(HttpRequest request) {
  149. _sendJson(request, {'message': 'Hello, World!'});
  150. }
  151. Future<World> _queryRandom() {
  152. return _connectionPool.connect().then((connection) {
  153. return connection.query(
  154. 'SELECT id, randomnumber FROM world WHERE id = @id;', {
  155. 'id': _RANDOM.nextInt(_WORLD_TABLE_SIZE) + 1
  156. })
  157. //
  158. // The benchmark's constraints tell us there is exactly one row.
  159. //
  160. .single.then((row) => new World(row[0], row[1])).whenComplete(() {
  161. connection.close();
  162. });
  163. });
  164. }
  165. /// Responds with the database query test to the [request].
  166. void _dbTest(HttpRequest request) {
  167. _queryRandom().then((response) => _sendJson(request, response));
  168. }
  169. /// Responds with the database queries test to the [request].
  170. void _queriesTest(HttpRequest request) {
  171. var queries = _parseInt(request.uri.queryParameters['queries']).clamp(1, 500);
  172. Future
  173. .wait(new List.generate(queries, (_) => _queryRandom(), growable: false))
  174. .then((response) => _sendJson(request, response));
  175. }
  176. /// Responds with the fortunes test to the [request].
  177. void _fortunesTest(HttpRequest request) {
  178. _connectionPool.connect().then((connection) {
  179. return connection
  180. .query('SELECT id, message FROM fortune;')
  181. .map((row) => new Fortune(row[0], row[1]))
  182. .toList()
  183. .whenComplete(() {
  184. connection.close();
  185. });
  186. }).then((fortunes) {
  187. fortunes.add(new Fortune(0, 'Additional fortune added at request time.'));
  188. fortunes.sort();
  189. _sendHtml(request, _fortunesTemplate.renderString({
  190. 'fortunes': fortunes
  191. .map((fortune) => {'id': fortune.id, 'message': fortune.message})
  192. .toList()
  193. }));
  194. });
  195. }
  196. /// Responds with the updates test to the [request].
  197. void _updatesTest(HttpRequest request) {
  198. var queries = _parseInt(request.uri.queryParameters['queries']).clamp(1, 500);
  199. Future.wait(new List.generate(queries, (_) {
  200. return _queryRandom().then((world) {
  201. world.randomnumber = _RANDOM.nextInt(_WORLD_TABLE_SIZE) + 1;
  202. return _connectionPool.connect().then((connection) {
  203. return connection
  204. .execute(
  205. 'UPDATE world SET randomnumber = @randomnumber WHERE id = @id;',
  206. {'randomnumber': world.randomnumber, 'id': world.id})
  207. .whenComplete(() {
  208. connection.close();
  209. });
  210. }).then((_) => world);
  211. });
  212. }, growable: false)).then((worlds) => _sendJson(request, worlds));
  213. }
  214. /// Responds with the plaintext test to the [request].
  215. void _plaintextTest(HttpRequest request) {
  216. _sendText(request, 'Hello, World!');
  217. }