StatusTable: Do not use std::partial_sort

Since std::partial_sort is C++17 only, do not use it for Ninja
which should only use C++11.

Implement the sorting feature using a max-priority-queue with
a fixed size, then reversing the results to get the sorted list
of older commands from the input map.

Fuchsia-Only: multiline-status
Change-Id: Iae7618d2513fe095b96bae3dfd8124bf9b0e15e3
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/github.com/ninja-build/ninja/+/1037772
Reviewed-by: Tyler Mandry <tmandry@google.com>
Commit-Queue: David Turner <digit@google.com>
diff --git a/src/status_table.cc b/src/status_table.cc
index 8c31d0b..9d56583 100644
--- a/src/status_table.cc
+++ b/src/status_table.cc
@@ -14,7 +14,7 @@
 
 #include "status_table.h"
 
-#include <algorithm>
+#include <cstdint>
 
 #include "assert.h"
 
@@ -23,7 +23,10 @@
 #endif
 
 StatusTable::StatusTable(const Config& config, AsyncLoop& async_loop)
-    : config_(config), async_loop_(async_loop) {}
+    : config_(config), async_loop_(async_loop) {
+  commands_max_queue_.reserve(config_.max_commands);
+  older_commands_.reserve(config_.max_commands);
+}
 
 StatusTable::~StatusTable() = default;
 
@@ -94,29 +97,49 @@
 }
 
 void StatusTable::PrintPending(int64_t cur_time_millis) {
-  if (!config_.max_commands)
+  size_t max_commands = config_.max_commands;
+  if (!max_commands)
     return;
 
-  // Find the N-th older running edges, where N is max_height_.
-  // Reuse the sorted_pending_edges_ vector between calls.
-  auto& sorted_commands = sorted_pending_commands_;
-  sorted_commands.assign(pending_commands_.begin(), pending_commands_.end());
+  // Find the |max_commands| older running edges, by using a fixed-size
+  // max-priority-queue. Then sort the result from oldest to newest
+  // commands.
 
-  auto less = [](const CommandInfo& a, const CommandInfo& b) -> bool {
-    return a.second < b.second;
-  };
-  size_t count = std::min(sorted_commands.size(), config_.max_commands);
+  // Fill queue with empty items to simplify loop below.
+  assert(commands_max_queue_.empty());
+  commands_max_queue_.resize(max_commands);
+  for (const auto& pair : pending_commands_) {
+    int64_t start_time_ms = pair.second;
+    // If this command is newer than the current top, ignore it
+    // otherwise replace the top with it in the queue.
+    if (start_time_ms < commands_max_queue_.top().start_time_ms) {
+      commands_max_queue_.pop();
+      commands_max_queue_.emplace(pair.first, start_time_ms);
+    }
+  }
 
-  std::partial_sort(sorted_commands.begin(), sorted_commands.begin() + count,
-                    sorted_commands.end(), less);
+  // Compute the older commands in _decreasing_ starting time,
+  // then parse it in reverse order for printing (to avoid an
+  // std::reverse() call).
+  older_commands_.clear();
+  while (!commands_max_queue_.empty()) {
+    const auto& info = commands_max_queue_.top();
+    // Ignore entries with |command == nullptr|, which happen
+    // when there are less than |config_.max_commands| items in
+    //|pending_commands_|.
+    if (info.command != nullptr)
+      older_commands_.push_back(info);
+    commands_max_queue_.pop();
+  }
 
   std::string pending_line;
-  for (size_t n = 0; n < count; ++n) {
-    CommandInfo& pair = sorted_commands[n];
 
+  for (auto info_reverse_it = older_commands_.end();
+       info_reverse_it != older_commands_.begin();) {
+    const CommandInfo& info = *(--info_reverse_it);
     // Format the elapsed time in a human friendly format.
     char elapsed_buffer[16];
-    int64_t elapsed_ms = cur_time_millis - pair.second;
+    int64_t elapsed_ms = cur_time_millis - info.start_time_ms;
     if (elapsed_ms < 0) {
       snprintf(elapsed_buffer, sizeof(elapsed_buffer), "??????");
     } else {
@@ -132,7 +155,7 @@
     }
 
     // Get edge description or command.
-    std::string description = GetCommandDescription(pair.first);
+    std::string description = GetCommandDescription(info.command);
 
     // Format '<elapsed> | <description>' where <elapsed> is
     // right-justified.
@@ -156,6 +179,7 @@
   }
 
   // Clear previous lines that are not needed anymore.
+  size_t count = older_commands_.size();
   size_t next_height = count;
   for (; count < last_command_count_; ++count) {
     ClearNextLine();
diff --git a/src/status_table.h b/src/status_table.h
index dbc27c2..c8acb0a 100644
--- a/src/status_table.h
+++ b/src/status_table.h
@@ -19,6 +19,7 @@
 #include <stdint.h>
 #include <stdio.h>
 
+#include <queue>
 #include <string>
 #include <unordered_map>
 #include <vector>
@@ -176,12 +177,30 @@
   using CommandMap = std::unordered_map<CommandPointer, int64_t>;
   CommandMap pending_commands_;
 
-  // Used on each PrintPending() call to minimize heap allocations.
-  // Note that CommandMap::value_type.first has type |const Edge* const| and
-  // thus CommandMap::value_type is not copyable and cannot be used as an
-  // std::vector<> item type.
-  using CommandInfo = std::pair<CommandPointer, int64_t>;
-  std::vector<CommandInfo> sorted_pending_commands_;
+  // A copyable alternative to CommandMap::value_type, with
+  // default values appropriate for the MaxQueue type below.
+  struct CommandInfo {
+    CommandInfo() = default;
+    CommandInfo(CommandPointer a_command, int64_t a_start_time_ms)
+        : command(a_command), start_time_ms(a_start_time_ms) {}
+
+    CommandPointer command = nullptr;
+    int64_t start_time_ms = INT64_MAX;
+
+    bool operator<(const CommandInfo& other) const {
+      return start_time_ms < other.start_time_ms;
+    }
+  };
+
+  // MaxQueue is a fixed-size max-heap of CommandInfo values, sorted by
+  // their start time (larger start time / newer command at the top).
+  struct MaxQueue : public std::priority_queue<CommandInfo> {
+    void reserve(size_t capacity) { c.reserve(capacity); }
+    void resize(size_t size) { c.resize(size); }
+  };
+
+  MaxQueue commands_max_queue_;
+  std::vector<CommandInfo> older_commands_;
 };
 
 #endif  // NINJA_STATUS_TABLE_H_