diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d67351e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -sudo: required - -services: - - docker - - postgresql - -language: ruby - -rvm: - - 2.7.1 - -before_script: - - cp config/database.yml.travis config/database.yml - - bundle exec rake db:create db:migrate - diff --git a/Gemfile b/Gemfile index b8cd5c5..e87f78e 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,7 @@ end gem 'pg', '>= 0.18', '< 2.0' gem 'rack', '~> 2.2' gem 'puma', '~> 8.0' -gem 'sass-rails' +gem 'sprockets-rails' gem 'ostruct' group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 336165e..58f4632 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -205,16 +205,6 @@ GEM tsort reline (0.7.0) io-console (~> 0.5) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt securerandom (0.4.1) sprockets (4.4.0) concurrent-ruby (~> 1.1) @@ -225,7 +215,6 @@ GEM activesupport (>= 6.1) sprockets (>= 3.0.0) thor (1.5.0) - tilt (2.9.0) timeout (0.6.1) tsort (0.2.0) tzinfo (2.0.6) @@ -256,7 +245,7 @@ DEPENDENCIES puma (~> 8.0) rack (~> 2.2) rails (~> 7.1.0) - sass-rails + sprockets-rails tzinfo-data web-console (>= 3.3.0) diff --git a/Gemfile.next.lock b/Gemfile.next.lock index e531565..ca0acf2 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -198,16 +198,6 @@ GEM tsort reline (0.7.0) io-console (~> 0.5) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt securerandom (0.4.1) sprockets (4.4.0) concurrent-ruby (~> 1.1) @@ -218,7 +208,6 @@ GEM activesupport (>= 6.1) sprockets (>= 3.0.0) thor (1.5.0) - tilt (2.9.0) timeout (0.6.1) tsort (0.2.0) tzinfo (2.0.6) @@ -250,7 +239,7 @@ DEPENDENCIES puma (~> 8.0) rack (~> 2.2) rails (~> 7.2.0) - sass-rails + sprockets-rails tzinfo-data web-console (>= 3.3.0) diff --git a/README.md b/README.md index 93d8272..8962af6 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,17 @@ one. Visit http://localhost:3000 and share a benchmark at it using the `SHARE_URL` example above. +To look at a report page without running a benchmark first, load the samples: + +```bash +bin/rails db:seed +``` + +That creates three reports covering the variations the page has to handle: a +comparison report with a noisy entry, one without `compare!`, and one from an +older client that sent no ruby/os/arch fields. It prints their paths and does +nothing if reports already exist, so pass `FORCE=1` to add them anyway. + ## Verifying a change end to end The suite uses Rack::Test, so it never exercises the real server or a real diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 5918193..1493c37 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,2 +1,3 @@ //= link_tree ../images //= link_directory ../stylesheets .css +//= link_tree ../fonts diff --git a/app/assets/fonts/README.md b/app/assets/fonts/README.md new file mode 100644 index 0000000..f57a406 --- /dev/null +++ b/app/assets/fonts/README.md @@ -0,0 +1,12 @@ +Self-hosted so the app makes no third-party request for fonts. + +Latin subsets pulled from Google Fonts. Source Code Pro is a variable font, so +one file covers every weight the app uses. + + oxygen-400.woff2 Oxygen Regular + oxygen-700.woff2 Oxygen Bold + source-code-pro.woff2 Source Code Pro, variable 200..900 + +Both families are licensed under the SIL Open Font License 1.1: +https://fonts.google.com/specimen/Oxygen/license +https://fonts.google.com/specimen/Source+Code+Pro/license diff --git a/app/assets/fonts/oxygen-400.woff2 b/app/assets/fonts/oxygen-400.woff2 new file mode 100644 index 0000000..45fca4e Binary files /dev/null and b/app/assets/fonts/oxygen-400.woff2 differ diff --git a/app/assets/fonts/oxygen-700.woff2 b/app/assets/fonts/oxygen-700.woff2 new file mode 100644 index 0000000..50d99f6 Binary files /dev/null and b/app/assets/fonts/oxygen-700.woff2 differ diff --git a/app/assets/fonts/source-code-pro.woff2 b/app/assets/fonts/source-code-pro.woff2 new file mode 100644 index 0000000..eff3376 Binary files /dev/null and b/app/assets/fonts/source-code-pro.woff2 differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js deleted file mode 100644 index e07c5a8..0000000 --- a/app/assets/javascripts/application.js +++ /dev/null @@ -1,16 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require jquery -//= require jquery_ujs -//= require turbolinks -//= require_tree . diff --git a/app/assets/javascripts/docs.coffee b/app/assets/javascripts/docs.coffee deleted file mode 100644 index 24f83d1..0000000 --- a/app/assets/javascripts/docs.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/reports.coffee b/app/assets/javascripts/reports.coffee deleted file mode 100644 index 24f83d1..0000000 --- a/app/assets/javascripts/reports.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index d784ea8..5ff2143 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1,21 +1,171 @@ /* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. + * Base styles and page furniture. Per-page styles live in their own files and + * are pulled in by require_tree below. * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. + * Colors and type follow fastruby.io: Oxygen for text, Source Code Pro for + * anything numeric, and square corners throughout. * *= require_tree . *= require_self */ +:root { + --ink: #262320; + --muted: #57514a; + --page: #f8f8f8; + --surface: #fff; + --cream: #f1ebe1; + --line: #dadada; + --faint: #6b6b6b; + --green: #00d242; + --green-pale: #b5e0c3; + --yellow: #fdca31; + --warn-ink: #8a6d3b; + --mono: "Source Code Pro", ui-monospace, SFMono-Regular, Menlo, monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", - Roboto, Oxygen-Sans, Ubuntu, Cantarell, - "Helvetica Neue", sans-serif; + margin: 0; + min-height: 100vh; + display: flex; + flex-direction: column; + background: var(--page); + color: var(--ink); + font-family: Oxygen, Helvetica, Arial, sans-serif; + font-size: 17px; + line-height: 1.45; + letter-spacing: 0.36px; +} + +a { + color: var(--ink); + text-decoration: none; + border-bottom: 1px solid var(--line); +} + +a:hover { + color: var(--green); + border-bottom-color: var(--green); +} + +code { + font-family: var(--mono); + font-size: 0.925em; +} + +/* Masthead */ + +.masthead { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + padding: 22px 20px; + background: var(--surface); + border-bottom: 1px solid var(--line); +} + +.masthead__wordmark { + font-weight: 700; + font-size: 20px; + letter-spacing: 1.18px; + border-bottom: 0; +} + +.masthead__wordmark span { + color: var(--green); +} + +.masthead__wordmark:hover { + color: var(--ink); +} + +.masthead__link { + font-size: 14px; + color: var(--muted); +} + +/* Page shell */ + +.shell { + flex: 1; + width: 100%; + max-width: 940px; + margin: 0 auto; + padding: 28px 20px 40px; +} + +/* Colophon */ + +.colophon { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 12px; + padding: 24px 20px; + background: var(--surface); + border-top: 1px solid var(--line); + font-size: 14px; + color: var(--muted); +} + +/* Shared bits */ + +.eyebrow { + margin: 0; + font-family: var(--mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 1.4px; + text-transform: uppercase; + color: var(--muted); +} + +/* Lines are separate elements rather than one
, so the markup can be
+   indented normally without leading whitespace reaching the page. */
+
+.terminal {
+  display: flex;
+  flex-direction: column;
+  background: var(--ink);
+  padding: 22px 24px;
+  overflow-x: auto;
+}
+
+.terminal__line {
+  font-family: var(--mono);
+  font-size: 14px;
+  line-height: 1.85;
+  color: var(--page);
+  white-space: nowrap;
+}
+
+.terminal__result {
+  margin-top: 1.85em;
+  color: var(--green-pale);
+}
+
+.terminal .prompt {
+  color: var(--green);
+}
+
+/* Wider screens opt into more generous spacing. Everything above is the
+   small-screen baseline. */
+
+@media screen and (min-width: 40em) {
+  .masthead,
+  .colophon {
+    padding-left: 32px;
+    padding-right: 32px;
+  }
+
+  .shell {
+    padding: 44px 32px 56px;
+  }
 }
diff --git a/app/assets/stylesheets/docs.css b/app/assets/stylesheets/docs.css
new file mode 100644
index 0000000..ebd0b86
--- /dev/null
+++ b/app/assets/stylesheets/docs.css
@@ -0,0 +1,89 @@
+/* Home page: hero, usage block, and the FAQ list. Flat selectors, no nesting. */
+
+.docs {
+  display: flex;
+  flex-direction: column;
+  gap: 32px;
+}
+
+.docs__hero {
+  display: flex;
+  flex-direction: column;
+  gap: 18px;
+  max-width: 62ch;
+}
+
+.docs__hero h1 {
+  margin: 0;
+  font-weight: 700;
+  font-size: 1.75rem;
+  line-height: 1.08;
+  letter-spacing: 1.18px;
+}
+
+.docs__hero p {
+  margin: 0;
+  font-size: 18px;
+  line-height: 1.55;
+  color: var(--muted);
+}
+
+.docs__usage {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  max-width: 74ch;
+}
+
+.docs__note {
+  margin: 0;
+  font-size: 14.5px;
+  color: var(--muted);
+}
+
+.docs__faq {
+  display: flex;
+  flex-direction: column;
+  border-top: 1px solid var(--line);
+  max-width: 78ch;
+}
+
+.docs__faq-item {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 8px;
+  padding: 22px 0;
+  border-bottom: 1px solid var(--line);
+}
+
+.docs__faq-item h2 {
+  margin: 0;
+  font-weight: 700;
+  font-size: 16px;
+  line-height: 1.35;
+  letter-spacing: normal;
+}
+
+.docs__faq-item p {
+  margin: 0;
+  font-size: 16px;
+  line-height: 1.55;
+  color: var(--muted);
+}
+
+/* Wider screens opt into the two-column FAQ and the larger heading. */
+
+@media screen and (min-width: 40em) {
+  .docs {
+    gap: 44px;
+  }
+
+  .docs__hero h1 {
+    font-size: 2.1rem;
+  }
+
+  .docs__faq-item {
+    grid-template-columns: 240px 1fr;
+    gap: 28px;
+  }
+}
diff --git a/app/assets/stylesheets/fonts.css.erb b/app/assets/stylesheets/fonts.css.erb
new file mode 100644
index 0000000..265e221
--- /dev/null
+++ b/app/assets/stylesheets/fonts.css.erb
@@ -0,0 +1,30 @@
+/*
+ * Self-hosted webfonts, so no third-party request is made on first load.
+ * .css.erb rather than .scss so the asset paths do not depend on the sass
+ * engine, which is on its way out.
+ */
+
+@font-face {
+  font-family: "Oxygen";
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url("<%= font_path "oxygen-400.woff2" %>") format("woff2");
+}
+
+@font-face {
+  font-family: "Oxygen";
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url("<%= font_path "oxygen-700.woff2" %>") format("woff2");
+}
+
+/* Variable font: one file serves every weight between 200 and 900. */
+@font-face {
+  font-family: "Source Code Pro";
+  font-style: normal;
+  font-weight: 200 900;
+  font-display: swap;
+  src: url("<%= font_path "source-code-pro.woff2" %>") format("woff2");
+}
diff --git a/app/assets/stylesheets/reports.css b/app/assets/stylesheets/reports.css
new file mode 100644
index 0000000..68b1153
--- /dev/null
+++ b/app/assets/stylesheets/reports.css
@@ -0,0 +1,257 @@
+/* Report page: the results table, the deviation notice, and the share block. */
+
+.report {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.report__head {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.report__meta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.report__meta li {
+  font-family: var(--mono);
+  font-size: 12.5px;
+  padding: 7px 12px;
+  background: var(--cream);
+  color: var(--muted);
+}
+
+/* The numeric columns do not wrap, so the table gets its own horizontal scroll
+   rather than pushing the page wide. */
+
+.results-wrap {
+  overflow-x: auto;
+  border: 1px solid var(--line);
+  background: var(--surface);
+}
+
+.results-wrap:focus-visible {
+  outline: 2px solid var(--green);
+  outline-offset: 2px;
+}
+
+.results {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--surface);
+}
+
+.results th {
+  font-family: var(--mono);
+  font-size: 11px;
+  font-weight: 500;
+  letter-spacing: 1.2px;
+  text-transform: uppercase;
+  color: var(--muted);
+  text-align: left;
+  padding: 12px 8px;
+  border-bottom: 2px solid var(--line);
+}
+
+.results td {
+  padding: 12px 8px;
+  border-top: 1px solid var(--line);
+  vertical-align: middle;
+}
+
+.results th:first-child,
+.results td:first-child {
+  padding-left: 14px;
+}
+
+.results th:last-child,
+.results td:last-child {
+  padding-right: 14px;
+}
+
+.results th.results__rail,
+.results td.results__rail {
+  width: 4px;
+  padding-left: 12px;
+  padding-right: 0;
+}
+
+.results__slower,
+.results__ips {
+  text-align: right;
+  font-family: var(--mono);
+  font-variant-numeric: tabular-nums;
+  white-space: nowrap;
+}
+
+.results__slower {
+  font-size: 14px;
+}
+
+.results__name {
+  min-width: 0;
+}
+
+.results__label {
+  font-family: var(--mono);
+  font-size: 15px;
+  font-weight: 300;
+}
+
+.results__tag {
+  font-family: var(--mono);
+  font-size: 10px;
+  font-weight: 500;
+  letter-spacing: 1.2px;
+  text-transform: uppercase;
+  color: var(--ink);
+  background: var(--green-pale);
+  padding: 3px 6px;
+  margin-left: 8px;
+  white-space: nowrap;
+}
+
+.results__value {
+  font-size: 16px;
+  font-weight: 300;
+}
+
+.results__dev {
+  font-size: 12.5px;
+  color: var(--faint);
+}
+
+.results__dev.is-noisy {
+  color: var(--warn-ink);
+}
+
+.results tr.is-fastest {
+  background: #f8fdf9;
+}
+
+.results tr.is-fastest td.results__rail {
+  background: var(--green);
+  background-clip: content-box;
+}
+
+.results tr.is-fastest .results__label,
+.results tr.is-fastest .results__value {
+  font-weight: 900;
+}
+
+.results tr.is-fastest .results__slower {
+  color: var(--faint);
+}
+
+.notice {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+  padding: 18px 22px;
+  background: var(--surface);
+  border: 1px solid var(--line);
+  border-left: 4px solid var(--yellow);
+}
+
+.notice p {
+  margin: 0;
+  font-size: 15.5px;
+  line-height: 1.5;
+  color: var(--muted);
+  max-width: 66ch;
+}
+
+.share {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.share__link {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 18px;
+}
+
+.share__url {
+  font-family: var(--mono);
+  font-size: 13.5px;
+  padding: 14px 16px;
+  background: var(--surface);
+  border: 1px solid var(--line);
+  user-select: all;
+  word-break: break-all;
+}
+
+.share__hint {
+  font-size: 13.5px;
+  color: var(--muted);
+}
+
+.share__snippet {
+  background: var(--surface);
+  border: 1px solid var(--line);
+}
+
+.share__snippet-head {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 16px;
+  background: var(--page);
+  border-bottom: 1px solid var(--line);
+}
+
+.share__snippet pre {
+  margin: 0;
+  padding: 18px 16px;
+  background: var(--ink);
+  color: var(--page);
+  font-family: var(--mono);
+  font-size: 13.5px;
+  line-height: 1.8;
+  overflow-x: auto;
+  user-select: all;
+}
+
+/* Wider screens opt into roomier cells. */
+
+@media screen and (min-width: 40em) {
+  .report {
+    gap: 30px;
+  }
+
+  .results th {
+    padding: 15px 10px;
+  }
+
+  .results td {
+    padding: 16px 10px;
+  }
+
+  .results th:first-child,
+  .results td:first-child {
+    padding-left: 22px;
+  }
+
+  .results th:last-child,
+  .results td:last-child {
+    padding-right: 22px;
+  }
+
+  .results th.results__rail,
+  .results td.results__rail {
+    padding-left: 18px;
+  }
+}
diff --git a/app/assets/stylesheets/reports.scss b/app/assets/stylesheets/reports.scss
deleted file mode 100644
index 4c50ea4..0000000
--- a/app/assets/stylesheets/reports.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-// Place all the styles related to the reports controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: http://sass-lang.com/
-
-.report {
-  .results {
-    font-family: Monaco, Consolas, 'Liberation Mono', Courier, monospace;
-  }
-  width: 600px;
-
-  @media screen and (max-width: 600px) {
-    width: 100%;
-    padding-right: 5px;
-    font-size: 13px;
-
-    .results {
-      font-size: 13px;
-    }
-
-  }
-}
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
deleted file mode 100644
index de6be79..0000000
--- a/app/helpers/application_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module ApplicationHelper
-end
diff --git a/app/helpers/docs_helper.rb b/app/helpers/docs_helper.rb
deleted file mode 100644
index 611f68b..0000000
--- a/app/helpers/docs_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module DocsHelper
-end
diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb
index 741e2d3..d2a59d1 100644
--- a/app/helpers/reports_helper.rb
+++ b/app/helpers/reports_helper.rb
@@ -18,7 +18,7 @@ def format_ips(value)
   def times_slower(best, cur)
     best_low = best["ips"] - best["stddev"]
     report_high = cur["ips"] + cur["stddev"]
-    overlaps = report_high > best_low 
+    overlaps = report_high > best_low
 
     if overlaps
       return "-"
@@ -34,4 +34,58 @@ def stddev_percentage(part)
   def format_stddev(part)
     "%4.1f%%" % stddev_percentage(part)
   end
+
+  # The report rendered as a markdown table, for pasting into an issue or a
+  # pull request. Mirrors what the page shows: the fastest entry is bolded and
+  # the slower column only appears for comparison reports.
+  def report_markdown(report, fastest, url)
+    columns = ["name", "iterations/second"]
+    columns << "slower" if report.compare
+
+    lines = []
+    lines << "| #{columns.join(" | ")} |"
+    lines << "| #{columns.map { "---" }.join(" | ")} |"
+
+    report.entries.each do |entry|
+      name = markdown_cell(entry["name"])
+      name = "**#{name}**" if fastest && entry["name"] == fastest["name"]
+
+      cells = [name, "#{format_ips(entry["ips"]).strip} \u00b1 #{format_stddev(entry).strip}"]
+      cells << times_slower(fastest, entry) if report.compare
+
+      lines << "| #{cells.join(" | ")} |"
+    end
+
+    environment = report_environment(report)
+    lines << ""
+    lines << [environment, "Full report: #{url}"].compact.join(". ")
+
+    lines.join("\n")
+  end
+
+  # "interpolation and 3 others - benchmark.fyi". A single-entry report gets just
+  # the name, since "and 0 others" reads like a bug.
+  def report_title(report, fastest)
+    name = fastest ? fastest["name"] : "Report"
+    others = report.entries.size - 1
+    name = "#{name} and #{pluralize(others, "other")}" if others.positive?
+
+    "#{name} - benchmark.fyi"
+  end
+
+  # Benchmark names are arbitrary strings. An unescaped pipe would add a column
+  # to the markdown table and break the row for everyone who pastes it.
+  def markdown_cell(value)
+    value.to_s.gsub("|") { "\\|" }
+  end
+
+  # "ruby 4.0.6, darwin/arm64" from whichever of those fields the client sent.
+  # Older reports predate them entirely, so this can be nil.
+  def report_environment(report)
+    ruby = "ruby #{report.ruby}" if report.ruby.present?
+    machine = [report.os, report.arch].reject(&:blank?).join("/")
+
+    parts = [ruby, machine.presence].compact
+    parts.any? ? parts.join(", ") : nil
+  end
 end
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
deleted file mode 100644
index 286b223..0000000
--- a/app/mailers/application_mailer.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-class ApplicationMailer < ActionMailer::Base
-  default from: 'from@example.com'
-  layout 'mailer'
-end
diff --git a/app/views/docs/index.html.erb b/app/views/docs/index.html.erb
index 8dd8f47..6b9bbee 100644
--- a/app/views/docs/index.html.erb
+++ b/app/views/docs/index.html.erb
@@ -1,21 +1,58 @@
-
+
+
+

Share benchmark results as a link

+

+ Run your benchmark-ips + benchmark with one extra environment variable and get a URL you can paste + into an issue, a pull request, or a blog post. +

+
+
+
How to use it
+
+
$ gem install benchmark-ips
+
$ SHARE=1 ruby my_benchmark.rb
+
Shared at: https://ips.fastruby.io/2Bq
+
+

+ Requires benchmark-ips 2.6.0 or greater. Add x.compare! to your + benchmark for the times-slower column. +

+
+ +
+
+

I shared a benchmark, how do I find it?

+

+ The URL is printed in your terminal after the run. Share it with friends, + enemies, whoever. +

+
+ +
+

Can I run my own instance?

+

+ Yes. Point benchmark-ips at it with SHARE_URL instead of + SHARE, and it posts there rather than here. +

+
+ +
+

The site is ugly, how can I make it nicer?

+

+ Send a pull request to + github.com/fastruby/benchmark.fyi + with your changes. +

+
+ +
+

I have a great idea to improve benchmark.fyi!

+

+ Excellent. Open an issue or send a pull request to + github.com/fastruby/benchmark.fyi. +

+
+
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 6447cb1..b2254cb 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -9,8 +9,26 @@ -

benchmark.fyi

+
+ benchmark.fyi + GitHub +
- <%= yield %> +
+ <%= yield %> +
+ + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb deleted file mode 100644 index cbd34d2..0000000 --- a/app/views/layouts/mailer.html.erb +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - <%= yield %> - - diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb deleted file mode 100644 index 37f0bdd..0000000 --- a/app/views/layouts/mailer.text.erb +++ /dev/null @@ -1 +0,0 @@ -<%= yield %> diff --git a/app/views/reports/show.html.erb b/app/views/reports/show.html.erb index 75cf7d7..dc8beae 100644 --- a/app/views/reports/show.html.erb +++ b/app/views/reports/show.html.erb @@ -1,34 +1,80 @@ +<% report_url = "#{root_url}#{@report.short_id}" %> +<% content_for :title, report_title(@report, @fastest) %> +
- - - - <% if @report.compare %> - - <% end %> - - - <% @report.entries.each do |part| %> - - +
nametimes sloweriterations/second
- <% if part["name"] == @fastest["name"] %> - <%= part["name"] %> - <% else %> - <%= part["name"] %> +
+

Shared report

+ <% if report_environment(@report) %> +
    + <% if @report.ruby.present? %>
  • ruby <%= @report.ruby %>
  • <% end %> + <% if @report.os.present? %>
  • <%= @report.os %>
  • <% end %> + <% if @report.arch.present? %>
  • <%= @report.arch %>
  • <% end %> +
+ <% end %> +
+ +
+ + + + + + <% if @report.compare %> + <% end %> - - <% if @report.compare %> - + + + + + <% @report.entries.each do |part| %> + <% fastest = part["name"] == @fastest["name"] %> + "> + + + <% if @report.compare %> + + <% end %> + + <% end %> - - - <% end %> -
nameslower<%= times_slower @fastest, part %>iterations / second
+ <%= part["name"] %> + <% if fastest %> + fastest + <% end %> + <%= times_slower @fastest, part %> + <%= format_ips(part["ips"]).strip %> + <% noisy = stddev_percentage(part) >= 5 %> + "> + ± <%= format_stddev(part).strip %><% if noisy %> high<% end %> + +
<%= format_ips part["ips"] %> ±<%= format_stddev part %>
+
+
<% if @note_high_stddev %> -
- One or more entries has a fairly high standard deviation. This means that - the iterations were not very consistent and were subject to effects outside - the code itself, such as machine load, GC, etc. -
+ <% end %> + +
+ + + +
diff --git a/bin/spring b/bin/spring deleted file mode 100755 index 7fe232c..0000000 --- a/bin/spring +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env ruby - -# This file loads spring without using Bundler, in order to be fast. -# It gets overwritten when you run the `spring binstub` command. - -unless defined?(Spring) - require 'rubygems' - require 'bundler' - - if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) - Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } - gem 'spring', match[1] - require 'spring/binstub' - end -end diff --git a/config/database.yml.travis b/config/database.yml.travis deleted file mode 100644 index d507ef0..0000000 --- a/config/database.yml.travis +++ /dev/null @@ -1,4 +0,0 @@ -test: - adapter: postgresql - database: travis_ci_test - diff --git a/db/seeds.rb b/db/seeds.rb index 1beea2a..ba81513 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,61 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). +# Sample reports, so the /:id page can be looked at without running a real +# benchmark first. Safe to run repeatedly: it does nothing when reports already +# exist unless you pass FORCE=1. # -# Examples: -# -# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) -# Character.create(name: 'Luke', movie: movies.first) +# bin/rails db:seed + +if Report.any? && !ENV["FORCE"] + puts "Reports already exist (#{Report.count}). Pass FORCE=1 to add the samples anyway." + exit +end + +def entry(name, ips, stddev) + { + "name" => name, + "central_tendency" => ips, + "ips" => ips, + "error" => stddev, + "stddev" => stddev, + "microseconds" => 2_000_000.0, + "iterations" => (ips * 2).round, + "cycles" => (ips / 10).round + } +end + +created = [] + +# A comparison report with one noisy entry, so the times-slower column and the +# high-deviation notice are both visible. +created << Report.create!( + report: [ + entry("interpolation", 12_481_000.0, 149_772.0), + entry("concat (+)", 9_204_000.0, 165_672.0), + entry("<< buffer", 4_117_000.0, 345_828.0), + entry("format", 1_902_000.0, 39_942.0) + ], + compare: true, + ruby: "4.0.6", + os: "darwin", + arch: "arm64" +) + +# No compare!, so the slower column is absent, and every entry is consistent, so +# there is no deviation notice. +created << Report.create!( + report: [ + entry("Hash#fetch", 8_940_000.0, 62_580.0), + entry("Hash#[]", 9_120_000.0, 72_960.0) + ], + ruby: "3.3.12", + os: "linux", + arch: "x86_64" +) + +# An older client that sent no environment fields, so the meta row is empty. +created << Report.create!( + report: [entry("nokogiri parse", 41_200.0, 1_648.0)], + compare: true +) + +puts "Created #{created.size} reports:" +created.each { |r| puts " /#{r.short_id}" } diff --git a/test/integration/docs_test.rb b/test/integration/docs_test.rb index a5fcab0..1f76e16 100644 --- a/test/integration/docs_test.rb +++ b/test/integration/docs_test.rb @@ -5,6 +5,7 @@ class DocsTest < ActionDispatch::IntegrationTest get "/" assert_equal 200, status - assert_select "li b", text: "What is this?" + assert_select "h1", text: "Share benchmark results as a link" + assert_select ".docs__faq-item h2", text: "Can I run my own instance?" end end diff --git a/test/integration/share_snippet_test.rb b/test/integration/share_snippet_test.rb new file mode 100644 index 0000000..1439c58 --- /dev/null +++ b/test/integration/share_snippet_test.rb @@ -0,0 +1,78 @@ +require 'test_helper' + +class ShareSnippetTest < ActionDispatch::IntegrationTest + def entry(name, ips, stddev) + { + name: name, + ips: ips, + stddev: stddev, + microseconds: 3322, + iterations: 221, + cycles: 16 + } + end + + test "renders a markdown table alongside the html table" do + report = Report.create!( + report: [entry("fast", 500.0, 1.0), entry("slow", 100.0, 1.0)], + compare: true, + ruby: "4.0.6", + os: "darwin", + arch: "arm64" + ) + + get "/#{report.short_id}" + + assert_equal 200, status + + # the table is still there; the snippet is in addition to it, not instead + assert_select "table.results" + + snippet = css_select(".share__snippet pre").first.text + + assert_includes snippet, "| name | iterations/second | slower |" + assert_includes snippet, "| --- | --- | --- |" + assert_includes snippet, "**fast**" + assert_includes snippet, "| slow |" + assert_includes snippet, "5.00x" + assert_includes snippet, "ruby 4.0.6, darwin/arm64" + assert_includes snippet, "/#{report.short_id}" + end + + test "omits the slower column from the snippet for non comparison reports" do + report = Report.create! report: [entry("only", 500.0, 1.0)] + + get "/#{report.short_id}" + + snippet = css_select(".share__snippet pre").first.text + + assert_includes snippet, "| name | iterations/second |" + refute_includes snippet, "slower" + end + + test "escapes pipes in names so they do not add markdown columns" do + report = Report.create! report: [entry("a | b", 500.0, 1.0)] + + get "/#{report.short_id}" + + snippet = css_select(".share__snippet pre").first.text + + assert_includes snippet, "a \\| b" + + # the row must still be two columns wide, not three: an unescaped pipe would + # add a cell and break the table for anyone who pastes it + row = snippet.lines.find { |l| l.include?("a \\| b") } + assert_equal 3, row.scan(/(?