Skip to content

Commit 2d7ee52

Browse files
committed
Move ElideMiddle() to its own source file.
Move the function to its own source file since it is no longer trivial (and will get optimized in future patches). + Add a new ElideMiddleInPlace() function that doesn't do anything if no elision is required + use it in status.cc. + Add a new elide_middle_perftest program to benchmark the performance of the function. On my machine, the 'avg' result is 159.9ms.
1 parent 60f901e commit 2d7ee52

10 files changed

+274
-127
lines changed

CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ add_library(libninja OBJECT
135135
src/deps_log.cc
136136
src/disk_interface.cc
137137
src/edit_distance.cc
138+
src/elide_middle.cc
138139
src/eval_env.cc
139140
src/graph.cc
140141
src/graphviz.cc
@@ -265,6 +266,7 @@ if(BUILD_TESTING)
265266
src/disk_interface_test.cc
266267
src/dyndep_parser_test.cc
267268
src/edit_distance_test.cc
269+
src/elide_middle_test.cc
268270
src/explanations_test.cc
269271
src/graph_test.cc
270272
src/json_test.cc
@@ -295,6 +297,7 @@ if(BUILD_TESTING)
295297
canon_perftest
296298
clparser_perftest
297299
depfile_parser_perftest
300+
elide_middle_perftest
298301
hash_collision_bench
299302
manifest_parser_perftest
300303
)

configure.py

+3
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ def has_re2c() -> bool:
539539
'dyndep',
540540
'dyndep_parser',
541541
'edit_distance',
542+
'elide_middle',
542543
'eval_env',
543544
'graph',
544545
'graphviz',
@@ -638,6 +639,7 @@ def has_re2c() -> bool:
638639
'disk_interface_test',
639640
'dyndep_parser_test',
640641
'edit_distance_test',
642+
'elide_middle_test',
641643
'explanations_test',
642644
'graph_test',
643645
'json_test',
@@ -683,6 +685,7 @@ def has_re2c() -> bool:
683685

684686
for name in ['build_log_perftest',
685687
'canon_perftest',
688+
'elide_middle_perftest',
686689
'depfile_parser_perftest',
687690
'hash_collision_bench',
688691
'manifest_parser_perftest',

src/elide_middle.cc

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2024 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "elide_middle.h"
16+
17+
#include <assert.h>
18+
#include <string.h>
19+
20+
#include <regex>
21+
22+
void ElideMiddleInPlace(std::string& str, size_t max_width) {
23+
if (max_width <= 3) {
24+
str.assign("...", max_width);
25+
return;
26+
}
27+
const int kMargin = 3; // Space for "...".
28+
const static std::regex ansi_escape("\\x1b[^m]*m");
29+
std::string result = std::regex_replace(str, ansi_escape, "");
30+
if (result.size() <= max_width)
31+
return;
32+
33+
int32_t elide_size = (max_width - kMargin) / 2;
34+
35+
std::vector<std::pair<int32_t, std::string>> escapes;
36+
size_t added_len = 0; // total number of characters
37+
38+
std::sregex_iterator it(str.begin(), str.end(), ansi_escape);
39+
std::sregex_iterator end;
40+
while (it != end) {
41+
escapes.emplace_back(it->position() - added_len, it->str());
42+
added_len += it->str().size();
43+
++it;
44+
}
45+
46+
std::string new_status =
47+
result.substr(0, elide_size) + "..." +
48+
result.substr(result.size() - elide_size - ((max_width - kMargin) % 2));
49+
50+
added_len = 0;
51+
// We need to put all ANSI escape codes back in:
52+
for (const auto& escape : escapes) {
53+
int32_t pos = escape.first;
54+
if (pos > elide_size) {
55+
pos -= result.size() - max_width;
56+
if (pos < static_cast<int32_t>(max_width) - elide_size) {
57+
pos = max_width - elide_size - (max_width % 2 == 0 ? 1 : 0);
58+
}
59+
}
60+
pos += added_len;
61+
new_status.insert(pos, escape.second);
62+
added_len += escape.second.size();
63+
}
64+
str = std::move(new_status);
65+
}

src/elide_middle.h

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2024 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef NINJA_ELIDE_MIDDLE_H_
16+
#define NINJA_ELIDE_MIDDLE_H_
17+
18+
#include <cstddef>
19+
#include <string>
20+
21+
/// Elide the given string @a str with '...' in the middle if the length
22+
/// exceeds @a max_width. Note that this handles ANSI color sequences
23+
/// properly (non-color related sequences are ignored, but using them
24+
/// would wreak the cursor position or terminal state anyway).
25+
void ElideMiddleInPlace(std::string& str, size_t max_width);
26+
27+
#endif // NINJA_ELIDE_MIDDLE_H_

src/elide_middle_perftest.cc

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2024 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include <stdint.h>
16+
#include <stdio.h>
17+
#include <stdlib.h>
18+
#include <string.h>
19+
20+
#include <vector>
21+
22+
#include "elide_middle.h"
23+
#include "metrics.h"
24+
25+
static const char* kTestInputs[] = {
26+
"01234567890123456789",
27+
"012345\x1B[0;35m67890123456789",
28+
"abcd\x1b[1;31mefg\x1b[0mhlkmnopqrstuvwxyz",
29+
};
30+
31+
int main() {
32+
std::vector<int> times;
33+
34+
int64_t kMaxTimeMillis = 5 * 1000;
35+
int64_t base_time = GetTimeMillis();
36+
37+
const int kRuns = 100;
38+
for (int j = 0; j < kRuns; ++j) {
39+
int64_t start = GetTimeMillis();
40+
if (start >= base_time + kMaxTimeMillis)
41+
break;
42+
43+
const int kNumRepetitions = 2000;
44+
for (int count = kNumRepetitions; count > 0; --count) {
45+
for (const char* input : kTestInputs) {
46+
size_t input_len = ::strlen(input);
47+
for (size_t max_width = input_len; max_width > 0; --max_width) {
48+
std::string str(input, input_len);
49+
ElideMiddleInPlace(str, max_width);
50+
}
51+
}
52+
}
53+
54+
int delta = (int)(GetTimeMillis() - start);
55+
times.push_back(delta);
56+
}
57+
58+
int min = times[0];
59+
int max = times[0];
60+
float total = 0;
61+
for (size_t i = 0; i < times.size(); ++i) {
62+
total += times[i];
63+
if (times[i] < min)
64+
min = times[i];
65+
else if (times[i] > max)
66+
max = times[i];
67+
}
68+
69+
printf("min %dms max %dms avg %.1fms\n", min, max, total / times.size());
70+
71+
return 0;
72+
}

src/elide_middle_test.cc

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2024 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "elide_middle.h"
16+
17+
#include "test.h"
18+
19+
namespace {
20+
21+
std::string ElideMiddle(const std::string& str, size_t width) {
22+
std::string result = str;
23+
ElideMiddleInPlace(result, width);
24+
return result;
25+
}
26+
27+
} // namespace
28+
29+
30+
TEST(ElideMiddle, NothingToElide) {
31+
std::string input = "Nothing to elide in this short string.";
32+
EXPECT_EQ(input, ElideMiddle(input, 80));
33+
EXPECT_EQ(input, ElideMiddle(input, 38));
34+
EXPECT_EQ("", ElideMiddle(input, 0));
35+
EXPECT_EQ(".", ElideMiddle(input, 1));
36+
EXPECT_EQ("..", ElideMiddle(input, 2));
37+
EXPECT_EQ("...", ElideMiddle(input, 3));
38+
}
39+
40+
TEST(ElideMiddle, ElideInTheMiddle) {
41+
std::string input = "01234567890123456789";
42+
EXPECT_EQ("...9", ElideMiddle(input, 4));
43+
EXPECT_EQ("0...9", ElideMiddle(input, 5));
44+
EXPECT_EQ("012...789", ElideMiddle(input, 9));
45+
EXPECT_EQ("012...6789", ElideMiddle(input, 10));
46+
EXPECT_EQ("0123...6789", ElideMiddle(input, 11));
47+
EXPECT_EQ("01234567...23456789", ElideMiddle(input, 19));
48+
EXPECT_EQ("01234567890123456789", ElideMiddle(input, 20));
49+
}
50+
51+
// A few ANSI escape sequences. These macros make the following
52+
// test easier to read and understand.
53+
#define MAGENTA "\x1B[0;35m"
54+
#define NOTHING "\33[m"
55+
#define RED "\x1b[1;31m"
56+
#define RESET "\x1b[0m"
57+
58+
TEST(ElideMiddle, ElideAnsiEscapeCodes) {
59+
std::string input = "012345" MAGENTA "67890123456789";
60+
EXPECT_EQ("012..." MAGENTA "6789", ElideMiddle(input, 10));
61+
EXPECT_EQ("012345" MAGENTA "67...23456789", ElideMiddle(input, 19));
62+
63+
EXPECT_EQ("Nothing " NOTHING " string.",
64+
ElideMiddle("Nothing " NOTHING " string.", 18));
65+
EXPECT_EQ("0" NOTHING "12...6789",
66+
ElideMiddle("0" NOTHING "1234567890123456789", 10));
67+
68+
input = "abcd" RED "efg" RESET "hlkmnopqrstuvwxyz";
69+
EXPECT_EQ("", ElideMiddle(input, 0));
70+
EXPECT_EQ(".", ElideMiddle(input, 1));
71+
EXPECT_EQ("..", ElideMiddle(input, 2));
72+
EXPECT_EQ("...", ElideMiddle(input, 3));
73+
EXPECT_EQ("..." RED RESET "z", ElideMiddle(input, 4));
74+
EXPECT_EQ("a..." RED RESET "z", ElideMiddle(input, 5));
75+
EXPECT_EQ("a..." RED RESET "yz", ElideMiddle(input, 6));
76+
EXPECT_EQ("ab..." RED RESET "yz", ElideMiddle(input, 7));
77+
EXPECT_EQ("ab..." RED RESET "xyz", ElideMiddle(input, 8));
78+
EXPECT_EQ("abc..." RED RESET "xyz", ElideMiddle(input, 9));
79+
EXPECT_EQ("abc..." RED RESET "wxyz", ElideMiddle(input, 10));
80+
EXPECT_EQ("abcd" RED "..." RESET "wxyz", ElideMiddle(input, 11));
81+
EXPECT_EQ("abcd" RED "..." RESET "vwxyz", ElideMiddle(input, 12));
82+
83+
EXPECT_EQ("abcd" RED "ef..." RESET "uvwxyz", ElideMiddle(input, 15));
84+
EXPECT_EQ("abcd" RED "ef..." RESET "tuvwxyz", ElideMiddle(input, 16));
85+
EXPECT_EQ("abcd" RED "efg" RESET "...tuvwxyz", ElideMiddle(input, 17));
86+
EXPECT_EQ("abcd" RED "efg" RESET "...stuvwxyz", ElideMiddle(input, 18));
87+
EXPECT_EQ("abcd" RED "efg" RESET "h...stuvwxyz", ElideMiddle(input, 19));
88+
89+
input = "abcdef" RED "A" RESET "BC";
90+
EXPECT_EQ("..." RED RESET "C", ElideMiddle(input, 4));
91+
EXPECT_EQ("a..." RED RESET "C", ElideMiddle(input, 5));
92+
EXPECT_EQ("a..." RED RESET "BC", ElideMiddle(input, 6));
93+
EXPECT_EQ("ab..." RED RESET "BC", ElideMiddle(input, 7));
94+
EXPECT_EQ("ab..." RED "A" RESET "BC", ElideMiddle(input, 8));
95+
EXPECT_EQ("abcdef" RED "A" RESET "BC", ElideMiddle(input, 9));
96+
}
97+
98+
#undef RESET
99+
#undef RED
100+
#undef NOTHING
101+
#undef MAGENTA

src/line_printer.cc

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <sys/time.h>
2929
#endif
3030

31+
#include "elide_middle.h"
3132
#include "util.h"
3233

3334
using namespace std;
@@ -81,7 +82,7 @@ void LinePrinter::Print(string to_print, LineType type) {
8182
CONSOLE_SCREEN_BUFFER_INFO csbi;
8283
GetConsoleScreenBufferInfo(console_, &csbi);
8384

84-
to_print = ElideMiddle(to_print, static_cast<size_t>(csbi.dwSize.X));
85+
ElideMiddleInPlace(to_print, static_cast<size_t>(csbi.dwSize.X));
8586
if (supports_color_) { // this means ENABLE_VIRTUAL_TERMINAL_PROCESSING
8687
// succeeded
8788
printf("%s\x1B[K", to_print.c_str()); // Clear to end of line.
@@ -108,7 +109,7 @@ void LinePrinter::Print(string to_print, LineType type) {
108109
// line-wrapping.
109110
winsize size;
110111
if ((ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) == 0) && size.ws_col) {
111-
to_print = ElideMiddle(to_print, size.ws_col);
112+
ElideMiddleInPlace(to_print, size.ws_col);
112113
}
113114
printf("%s", to_print.c_str());
114115
printf("\x1B[K"); // Clear to end of line.

0 commit comments

Comments
 (0)