CCTLib
Calling-context and data-object attribution library for Intel Pin
redspy_integration_test.cpp
Go to the documentation of this file.
1 // GoogleTest-driven integration tests for redspy_client.
2 //
3 // Semantics: redspy reports "redundant writes" -- stores whose value is
4 // identical to the value already in memory at the target location. The
5 // output format is text; the primary summary line is
6 // " Total redundant bytes = <pct> %"
7 // (leading space; note that the summary is emitted once per instrumented
8 // thread, and thread 0 is the process's main thread).
9 //
10 // Test design: build small victim programs that either (TP) write the same
11 // value twice to the same location or (TN) write different values, then
12 // compare the redundancy percentage. TP should be strictly higher than TN,
13 // with a margin well above the ~few percent noise from libc startup.
14 
15 #include <cerrno>
16 #include <cstdio>
17 #include <cstdlib>
18 #include <cstring>
19 #include <fcntl.h>
20 #include <fstream>
21 #include <regex>
22 #include <sstream>
23 #include <string>
24 #include <sys/wait.h>
25 #include <unistd.h>
26 #include <vector>
27 
28 #include <gtest/gtest.h>
29 
30 namespace {
31 
32 std::string env(const char* name) { const char* v = getenv(name); return v ? v : ""; }
33 std::string cctlib_root() { std::string r = env("CCTLIB_ROOT"); return r.empty() ? "../.." : r; }
34 std::string pin_root() { return env("PIN_ROOT"); }
35 
36 int run_pin(const std::string& tool, const std::vector<std::string>& args) {
37  std::string pin = pin_root() + "/pin";
38  std::vector<char*> argv;
39  argv.push_back(const_cast<char*>(pin.c_str()));
40  argv.push_back(const_cast<char*>("-t"));
41  argv.push_back(const_cast<char*>(tool.c_str()));
42  argv.push_back(const_cast<char*>("--"));
43  for (auto& a : args) argv.push_back(const_cast<char*>(a.c_str()));
44  argv.push_back(nullptr);
45 
46  pid_t pid = fork();
47  if (pid < 0) return -1;
48  if (pid == 0) {
49  if (!env("CCTLIB_TEST_VERBOSE").size()) {
50  int devnull = open("/dev/null", O_WRONLY);
51  if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); }
52  }
53  execv(pin.c_str(), argv.data());
54  _exit(127);
55  }
56  int status = 0;
57  if (waitpid(pid, &status, 0) < 0) return -1;
58  if (WIFEXITED(status)) return WEXITSTATUS(status);
59  return -2;
60 }
61 
62 std::string find_newest(const std::string& dir, const std::string& prefix) {
63  std::string cmd = "ls -t " + dir + "/" + prefix + "* 2>/dev/null | head -1";
64  FILE* p = popen(cmd.c_str(), "r");
65  if (!p) return {};
66  char buf[4096]; std::string out;
67  if (fgets(buf, sizeof(buf), p)) out = buf;
68  pclose(p);
69  if (!out.empty() && out.back() == '\n') out.pop_back();
70  return out;
71 }
72 std::string read_file(const std::string& path) {
73  std::ifstream in(path); std::stringstream ss; ss << in.rdbuf(); return ss.str();
74 }
75 void cleanup(const std::string& dir, const std::string& prefix) {
76  std::string cmd = "rm -f " + dir + "/" + prefix + "*"; (void)system(cmd.c_str());
77 }
78 
79 // Parses the FIRST "Total redundant bytes = <pct> %" line (thread 0's
80 // summary). Returns -1 if not present.
81 double parse_first_redundant_pct(const std::string& content) {
82  std::regex re(R"(Total redundant bytes = ([\d.]+)\s*%)");
83  std::smatch m;
84  if (!std::regex_search(content, m, re)) return -1;
85  return std::stod(m[1]);
86 }
87 
88 class RedspyIntegration : public ::testing::Test {
89  protected:
90  std::string root_;
91  std::string tool_;
92  void SetUp() override {
93  root_ = cctlib_root();
94  ASSERT_FALSE(pin_root().empty()) << "PIN_ROOT required";
95  tool_ = root_ + "/clients/obj-intel64/redspy_client.so";
96  ASSERT_EQ(0, access(tool_.c_str(), F_OK)) << tool_ << " missing";
97  ASSERT_EQ(0, chdir(root_.c_str())) << "chdir failed";
98  }
99  // Returns the redundant-bytes percentage from thread-0's summary.
100  double run_and_parse_pct(const std::string& victim) {
101  cleanup(root_, "redspy.out.");
102  int rc = run_pin(tool_, {victim});
103  EXPECT_EQ(0, rc) << "redspy on " << victim << " returned " << rc;
104  std::string out = find_newest(root_, "redspy.out.");
105  EXPECT_FALSE(out.empty()) << "no redspy.out.* file";
107  }
108 };
109 
110 // Sanity: redspy runs cleanly on /bin/ls.
111 TEST_F(RedspyIntegration, RunsCleanlyOnLs) {
112  double pct = run_and_parse_pct("/bin/ls");
113  ASSERT_GE(pct, 0.0); // format present -- may be 0 if nothing redundant
114  EXPECT_LT(pct, 100.0); // sanity
115 }
116 
117 // TP vs TN: writes the same value vs writes different values.
118 // TP victim's workload alone is ~10K pair-writes of the same value; TN
119 // victim's workload has zero same-value pairs from its workload.
120 // Startup noise adds a few percent redundant bytes in both; the WORKLOAD
121 // difference should be clearly visible.
122 TEST_F(RedspyIntegration, TruePositiveHigherThanTrueNegative) {
123  double tp = run_and_parse_pct(root_ + "/tests/gtest/obj/apps/redspy_tp_simple");
124  double tn = run_and_parse_pct(root_ + "/tests/gtest/obj/apps/redspy_tn_simple");
125  ASSERT_GE(tp, 0.0);
126  ASSERT_GE(tn, 0.0);
127  // TP: 10000 pairs of 8B same-value writes = 80000 redundant bytes.
128  // Baseline libc redundancy is ~40% and each victim writes roughly the
129  // same volume, so TP-pct should exceed TN-pct by at least a few
130  // percentage points from workload alone.
131  EXPECT_GT(tp, tn + 2.0)
132  << "TP=" << tp << "% TN=" << tn << "% -- expected TP > TN + 2%";
133 }
134 
135 // Multi-size TP should also exceed TN.
136 TEST_F(RedspyIntegration, MultiSizeTruePositiveExceedsTN) {
137  double tp = run_and_parse_pct(root_ + "/tests/gtest/obj/apps/redspy_tp_sizes");
138  double tn = run_and_parse_pct(root_ + "/tests/gtest/obj/apps/redspy_tn_simple");
139  ASSERT_GE(tp, 0.0);
140  ASSERT_GE(tn, 0.0);
141  EXPECT_GT(tp, tn + 2.0) << "TP-sizes=" << tp << "% TN=" << tn << "%";
142 }
143 
144 // Symbol attribution: the top redspy report contexts should reference the
145 // victim's store8 function for the TP victim.
146 TEST_F(RedspyIntegration, TPReportAttributesToStore8) {
147  cleanup(root_, "redspy.out.");
148  int rc = run_pin(tool_, {root_ + "/tests/gtest/obj/apps/redspy_tp_simple"});
149  ASSERT_EQ(0, rc);
150  std::string out = find_newest(root_, "redspy.out.");
151  ASSERT_FALSE(out.empty());
152  std::string cmd = "grep -c 'store8:.*redspy_tp_simple.c' " + out;
153  FILE* p = popen(cmd.c_str(), "r");
154  ASSERT_NE(p, nullptr);
155  char buf[64]; std::string s;
156  if (fgets(buf, sizeof(buf), p)) s = buf;
157  pclose(p);
158  long hits = s.empty() ? 0 : std::stol(s);
159  EXPECT_GT(hits, 0)
160  << "redspy report has no context mentioning store8 in redspy_tp_simple.c";
161 }
162 
163 // ------------------------------------------------------------------
164 // ISA breadth tests. Each victim writes the SAME value TWICE to the same
165 // address via a different instruction class. Every second write is
166 // redundant. We assert each ISA victim's redundancy percentage exceeds
167 // the TN-simple baseline (which has zero workload-attributable
168 // redundancy) by a healthy margin.
169 
170 struct RedspyIsaVictim { const char* name; double min_extra_pct; };
171 
173  public ::testing::WithParamInterface<RedspyIsaVictim> {};
174 
175 TEST_P(RedspyIsa, ExceedsBaseline) {
176  double tn = run_and_parse_pct(root_ + "/tests/gtest/obj/apps/redspy_tn_simple");
177  double isa = run_and_parse_pct(root_ + "/tests/gtest/obj/apps/isa/" + GetParam().name);
178  ASSERT_GE(tn, 0.0);
179  ASSERT_GE(isa, 0.0);
180  EXPECT_GT(isa - tn, GetParam().min_extra_pct)
181  << "victim=" << GetParam().name
182  << " isa-pct=" << isa
183  << "% tn-pct=" << tn
184  << "% threshold=" << GetParam().min_extra_pct;
185 }
186 
188  IsaBreadth, RedspyIsa,
189  ::testing::Values(
190  // 128-bit same-value stores (movdqa xmm, mem twice).
191  RedspyIsaVictim{"redspy_sse16_tp", 2.0},
192  // 256-bit same-value stores (vmovdqu ymm, mem twice).
193  RedspyIsaVictim{"redspy_avx32_tp", 2.0},
194  // 32-bit same-immediate stores.
195  RedspyIsaVictim{"redspy_immediate_tp", 1.0},
196  // Partial overlap where the byte value matches the qword's low byte.
197  RedspyIsaVictim{"redspy_partial_qword_then_byte_tp", 0.3},
198  // SIMD zero-fill of the same 16 bytes twice (pxor + movdqu twice).
199  // Real-world memset-then-memset-again pattern.
200  RedspyIsaVictim{"redspy_zero_fill_tp", 2.0},
201  // Scalar SSE movsd (8B FP scalar move) same value twice.
202  RedspyIsaVictim{"redspy_scalar_sse_tp", 1.0},
203  // Cross-page same-value 8B write. Straddles a 4KB boundary --
204  // exercises redspy's cross-boundary shadow bookkeeping.
205  RedspyIsaVictim{"redspy_cross_page_tp", 0.5}),
206  [](const testing::TestParamInfo<RedspyIsaVictim>& info) {
207  return info.param.name;
208  });
209 
210 } // namespace
static uint64_t buf[ITERS]
volatile uint8_t status
Definition: cctlib.cpp:230
std::string find_newest(const std::string &dir, const std::string &prefix)
void cleanup(const std::string &dir, const std::string &prefix)
double parse_first_redundant_pct(const std::string &content)
TEST_F(RedspyIntegration, TPReportAttributesToStore8)
INSTANTIATE_TEST_SUITE_P(IsaBreadth, RedspyIsa, ::testing::Values(RedspyIsaVictim{"redspy_sse16_tp", 2.0}, RedspyIsaVictim{"redspy_avx32_tp", 2.0}, RedspyIsaVictim{"redspy_immediate_tp", 1.0}, RedspyIsaVictim{"redspy_partial_qword_then_byte_tp", 0.3}, RedspyIsaVictim{"redspy_zero_fill_tp", 2.0}, RedspyIsaVictim{"redspy_scalar_sse_tp", 1.0}, RedspyIsaVictim{"redspy_cross_page_tp", 0.5}), [](const testing::TestParamInfo< RedspyIsaVictim > &info) { return info.param.name;})
int run_pin(const std::string &tool, const std::vector< std::string > &args)
int a[1000]
Definition: testArray.c:5