File: userspace_swap_policy_chromeos_unittest.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (488 lines) | stat: -rw-r--r-- 19,571 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/performance_manager/policies/userspace_swap_policy_chromeos.h"

#include "base/allocator/buildflags.h"
#include "base/compiler_specific.h"
#include "base/memory/raw_ptr.h"
#include "base/system/sys_info.h"
#include "base/time/time.h"
#include "chrome/browser/performance_manager/policies/policy_features.h"
#include "chromeos/ash/components/memory/userspace_swap/userspace_swap.h"
#include "components/performance_manager/graph/graph_impl.h"
#include "components/performance_manager/graph/graph_impl_operations.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/performance_manager_impl.h"
#include "components/performance_manager/test_support/graph_test_harness.h"
#include "components/performance_manager/test_support/mock_graphs.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_render_thread.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace performance_manager {
namespace policies {

namespace {
using ::ash::memory::userspace_swap::UserspaceSwapConfig;
using testing::_;
using testing::Invoke;
using testing::Return;
using testing::StrictMock;

class MockUserspaceSwapPolicy : public UserspaceSwapPolicy {
 public:
  MockUserspaceSwapPolicy() : UserspaceSwapPolicy(InitTestConfig()) {}

  MockUserspaceSwapPolicy(const MockUserspaceSwapPolicy&) = delete;
  MockUserspaceSwapPolicy& operator=(const MockUserspaceSwapPolicy&) = delete;

  ~MockUserspaceSwapPolicy() override = default;

  MOCK_METHOD0(SwapNodesOnGraph, void(void));
  MOCK_METHOD1(InitializeProcessNode, bool(const ProcessNode*));
  MOCK_METHOD2(IsEligibleToSwap, bool(const ProcessNode*, const PageNode*));
  MOCK_METHOD1(SwapProcessNode, void(const ProcessNode*));
  MOCK_METHOD0(GetSwapDeviceFreeSpaceBytes, uint64_t(void));
  MOCK_METHOD0(GetTotalSwapFileUsageBytes, uint64_t(void));
  MOCK_METHOD1(GetProcessNodeSwapFileUsageBytes, uint64_t(const ProcessNode*));
  MOCK_METHOD1(IsPageNodeAudible, bool(const PageNode*));
  MOCK_METHOD1(IsPageNodeVisible, bool(const PageNode*));
  MOCK_METHOD1(IsPageNodeLoading, bool(const PageNode*));
  MOCK_METHOD1(GetLastVisibilityChangeTime, base::TimeTicks(const PageNode*));

  // Allow our mock to dispatch to default implementations.
  bool DefaultIsEligibleToSwap(const ProcessNode* process_node,
                               const PageNode* page_node) {
    return UserspaceSwapPolicy::IsEligibleToSwap(process_node, page_node);
  }

  bool DefaultInitializeProcessNode(const ProcessNode* process_node) {
    return UserspaceSwapPolicy::InitializeProcessNode(process_node);
  }

  void DefaultSwapNodesOnGraph() {
    return UserspaceSwapPolicy::SwapNodesOnGraph();
  }

  base::TimeTicks get_last_graph_walk() { return last_graph_walk_; }
  void set_last_graph_walk(base::TimeTicks t) { last_graph_walk_ = t; }

  // We allow tests to modify the config for testing individual behaviors.
  UserspaceSwapConfig& config() { return test_config_; }

 private:
  const UserspaceSwapConfig& InitTestConfig() {
    // Create a simple starting config that can be modified as needed for tests.
    // NOTE: We only initialize the configuration options which are used by the
    // policy.
    UNSAFE_TODO(memset(&test_config_, 0, sizeof(test_config_)));

    test_config_.enabled = true;
    test_config_.graph_walk_frequency = base::Seconds(10);
    test_config_.invisible_time_before_swap = base::Seconds(30);
    test_config_.process_swap_frequency = base::Seconds(60);
    test_config_.swap_on_freeze = true;
    test_config_.swap_on_moderate_pressure = true;
    return test_config_;
  }

  UserspaceSwapConfig test_config_ = {};
};

class UserspaceSwapPolicyTest : public ::testing::Test {
 public:
  UserspaceSwapPolicyTest()
      : browser_env_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

  UserspaceSwapPolicyTest(const UserspaceSwapPolicyTest&) = delete;
  UserspaceSwapPolicyTest& operator=(const UserspaceSwapPolicyTest&) = delete;

  ~UserspaceSwapPolicyTest() override = default;

  void SetUp() override {
    if (!base::SysInfo::IsRunningOnChromeOS()) {
      GTEST_SKIP() << "Skip test on chromeos-linux";
    }

    graph_ = std::make_unique<TestGraphImpl>();
    graph_->SetUp();

    CreateAndPassMockPolicy();

    // Create a simple graph.
    process_node_ = CreateNode<ProcessNodeImpl>();
    page_node_ = CreateNode<PageNodeImpl>();
    frame_node_ =
        graph()->CreateFrameNodeAutoId(process_node().get(), page_node().get());
    system_node_ = std::make_unique<TestNodeWrapper<SystemNodeImpl>>(
        TestNodeWrapper<SystemNodeImpl>::Create(graph()));
  }

  void AttachProcess() {
    // Create a process so this process node doesn't bail on Process.IsValid();
    process_node()->SetProcess(base::Process::Current(),
                               /* launch_time=*/base::TimeTicks::Now());
  }

  void TearDown() override {
    if (!base::SysInfo::IsRunningOnChromeOS()) {
      // Also skip TearDown() if SetUp() was skipped.
      return;
    }
    base::RunLoop().RunUntilIdle();

    policy_ = nullptr;
    frame_node_.reset();
    page_node_.reset();
    process_node_.reset();
    system_node_.reset();
    graph_->TearDown();
    graph_ = nullptr;
  }

  void CreateAndPassMockPolicy() {
    // Add our mock policy to the graph.
    std::unique_ptr<StrictMock<MockUserspaceSwapPolicy>> mock_policy(
        new StrictMock<MockUserspaceSwapPolicy>);
    policy_ = mock_policy.get();
    graph()->PassToGraph(std::move(mock_policy));
  }

  MockUserspaceSwapPolicy* policy() { return policy_; }

  // "borrowed" helper methods from the GraphTestHarness.
  template <class NodeClass, typename... Args>
  TestNodeWrapper<NodeClass> CreateNode(Args&&... args) {
    return TestNodeWrapper<NodeClass>::Create(graph(),
                                              std::forward<Args>(args)...);
  }

  TestGraphImpl* graph() { return graph_.get(); }
  content::BrowserTaskEnvironment* browser_env() { return &browser_env_; }
  TestNodeWrapper<ProcessNodeImpl>& process_node() { return process_node_; }
  TestNodeWrapper<PageNodeImpl>& page_node() { return page_node_; }
  TestNodeWrapper<FrameNodeImpl>& frame_node() { return frame_node_; }
  TestNodeWrapper<SystemNodeImpl>& system_node() {
    return *(system_node_.get());
  }

  void FastForwardBy(base::TimeDelta delta) {
    browser_env()->FastForwardBy(delta);
  }

  void RunUntilIdle() { browser_env()->RunUntilIdle(); }

 private:
  content::BrowserTaskEnvironment browser_env_;
  std::unique_ptr<TestGraphImpl> graph_;
  raw_ptr<MockUserspaceSwapPolicy> policy_ = nullptr;  // Not owned.

  TestNodeWrapper<ProcessNodeImpl> process_node_;
  TestNodeWrapper<PageNodeImpl> page_node_;
  TestNodeWrapper<FrameNodeImpl> frame_node_;
  std::unique_ptr<TestNodeWrapper<SystemNodeImpl>> system_node_;
};

// This test validates that we only initialize a ProcessNode once.
TEST_F(UserspaceSwapPolicyTest, ValidateInitializeProcessOnlyOnce) {
  EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get())).Times(1);

  // Attaching is a lifecycle state change.
  AttachProcess();

  // And now force another life cycle state change.
  process_node()->SetProcessExitStatus(0);
}

// This test validates that we only walk the graph under moderate pressure.
TEST_F(UserspaceSwapPolicyTest, ValidateGraphWalkFrequencyNoPressure) {
  auto last_walk_time = base::TimeTicks::Now();
  policy()->config().graph_walk_frequency = base::Seconds(1);
  policy()->config().swap_on_moderate_pressure = true;
  policy()->set_last_graph_walk(last_walk_time);

  // SwapNodesOnGraph shouldn't be called.
  EXPECT_CALL(*policy(), SwapNodesOnGraph()).Times(0);

  // We will fast forward by 100 graph walk frequencies, but since we're not
  // under pressure we will expect no calls.
  FastForwardBy(100 * policy()->config().graph_walk_frequency);

  // Confirm through the last_graph_walk time that we didn't walk.
  ASSERT_EQ(last_walk_time, policy()->get_last_graph_walk());
}

// This test validates that we only call WalkGraph every graph walk frequency
// seconds when under moderate pressure.
TEST_F(UserspaceSwapPolicyTest, ValidateGraphWalkFrequencyModeratePressure) {
  policy()->config().graph_walk_frequency = base::Seconds(60);

  // We expect that we will call SwapNodesOnGraph only 2 times.
  EXPECT_CALL(*policy(), SwapNodesOnGraph()).Times(2);

  // Triger memory pressure and we should observe the walk since we've never
  // walked before.
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  auto initial_walk_time = base::TimeTicks::Now();
  FastForwardBy(base::Seconds(1));
  ASSERT_EQ(initial_walk_time, policy()->get_last_graph_walk());

  // We will fast forward less than the graph walk frequency and confirm we
  // don't walk again even when we receive another moderate pressure
  // notification.
  FastForwardBy(base::Seconds(1));
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  // Since it's been less than the graph walk frequency we don't expect to walk.
  ASSERT_EQ(initial_walk_time, policy()->get_last_graph_walk());

  // Finally we will advance by a graph walk frequency and confirm we walk
  // again.
  FastForwardBy(policy()->config().graph_walk_frequency);
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);

  FastForwardBy(base::Seconds(1));
  ASSERT_NE(initial_walk_time, policy()->get_last_graph_walk());
}

// Validate we don't swap when not eligible.
TEST_F(UserspaceSwapPolicyTest, OnlySwapWhenEligibleToSwap) {
  policy()->config().graph_walk_frequency = base::Seconds(60);

  // Dispatch to the default swap nodes on graph implementation.
  EXPECT_CALL(*policy(), SwapNodesOnGraph())
      .WillOnce(
          Invoke(policy(), &MockUserspaceSwapPolicy::DefaultSwapNodesOnGraph));

  // We will say this node is not eligible to swap.
  EXPECT_CALL(*policy(),
              IsEligibleToSwap(process_node().get(), page_node().get()))
      .WillOnce(Return(false));

  // And we will expect that SwapProcessNode is NOT called.
  EXPECT_CALL(*policy(), SwapProcessNode(process_node().get())).Times(0);

  // Trigger moderate memory pressure to start the graph walk.
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  FastForwardBy(base::Seconds(1));
}

TEST_F(UserspaceSwapPolicyTest, OnlySwapWhenEligibleToSwapTrue) {
  // Dispatch to the default swap nodes on graph implementation.
  EXPECT_CALL(*policy(), SwapNodesOnGraph())
      .WillOnce(
          Invoke(policy(), &MockUserspaceSwapPolicy::DefaultSwapNodesOnGraph));

  // We will say this node is eligible to swap.
  EXPECT_CALL(*policy(),
              IsEligibleToSwap(process_node().get(), page_node().get()))
      .WillOnce(Return(true));

  // And we will expect that SwapProcessNode is called.
  EXPECT_CALL(*policy(), SwapProcessNode(process_node().get())).Times(1);

  // Trigger moderate memory pressure to start the graph walk.
  system_node()->OnMemoryPressureForTesting(
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  FastForwardBy(base::Seconds(1));
}

// This test validates that we won't swap a node when it's visible.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenVisible) {
  // We will only swap a renderer once every 3 graph walks.
  policy()->config().graph_walk_frequency = base::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(1);

  EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
      .WillOnce(Return(true));
  AttachProcess();

  EXPECT_CALL(*policy(), IsPageNodeLoading(page_node().get()))
      .WillOnce(Return(false));

  // Because this page node is visible it should not be eligible for swap.
  EXPECT_CALL(*policy(), IsPageNodeVisible(page_node().get()))
      .WillOnce(Return(true));

  EXPECT_FALSE(policy()->DefaultIsEligibleToSwap(process_node().get(),
                                                 page_node().get()));
}

// This test validates that we won't swap a node when it's audible.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenAudible) {
  // We will only swap a renderer once every 3 graph walks.
  policy()->config().graph_walk_frequency = base::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(1);

  EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
      .WillOnce(Return(true));
  AttachProcess();

  EXPECT_CALL(*policy(), IsPageNodeLoading(page_node().get()))
      .WillOnce(Return(false));
  EXPECT_CALL(*policy(), IsPageNodeVisible(page_node().get()))
      .WillOnce(Return(false));

  // Because this page node is audible it won't be swappable.
  EXPECT_CALL(*policy(), IsPageNodeAudible(page_node().get()))
      .WillOnce(Return(true));

  EXPECT_FALSE(policy()->DefaultIsEligibleToSwap(process_node().get(),
                                                 page_node().get()));
}

// This test validates that we won't swap a node when it's loading.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenLoading) {
  // We will only swap a renderer once every 3 graph walks.
  policy()->config().graph_walk_frequency = base::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(1);

  EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
      .WillOnce(Return(true));
  AttachProcess();

  // Because this page node is loading it should not be eligible for swap.
  EXPECT_CALL(*policy(), IsPageNodeLoading(page_node().get()))
      .WillOnce(Return(true));

  EXPECT_FALSE(policy()->DefaultIsEligibleToSwap(process_node().get(),
                                                 page_node().get()));
}

// This test validates that we do not swap an individual process node more than
// the configuration allows.
TEST_F(UserspaceSwapPolicyTest, ValidateProcessSwapFrequency) {
  // We will only swap a renderer once every 3 graph walks.
  policy()->config().graph_walk_frequency = base::Seconds(1);
  policy()->config().process_swap_frequency =
      3 * policy()->config().graph_walk_frequency;

  EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
      .WillOnce(Return(true));
  AttachProcess();

  // Make sure this node is not visible, audible, or loading, and has been
  // invisible for a long time because this test isn't validating those things.
  EXPECT_CALL(*policy(), IsPageNodeAudible(page_node().get()))
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*policy(), IsPageNodeLoading(page_node().get()))
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*policy(), IsPageNodeVisible(page_node().get()))
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*policy(), GetLastVisibilityChangeTime(page_node().get()))
      .WillRepeatedly(Return(base::TimeTicks::Max()));

  EXPECT_CALL(*policy(), SwapNodesOnGraph())
      .WillRepeatedly(
          Invoke(policy(), &MockUserspaceSwapPolicy::DefaultSwapNodesOnGraph));

  // Invoke the standard IsEligibleToSwapChecks.
  EXPECT_CALL(*policy(),
              IsEligibleToSwap(process_node().get(), page_node().get()))
      .WillRepeatedly(
          Invoke(policy(), &MockUserspaceSwapPolicy::DefaultIsEligibleToSwap));

  // And although we will repeatedly walk the graph, we should only attempt to
  // swap this process node exactly one time.
  EXPECT_CALL(*policy(), SwapProcessNode(process_node().get())).Times(1);

  // We will walk the graph 3 times but this should only result in a single
  // swap.
  for (int i = 0; i < 3; ++i) {
    FastForwardBy(policy()->config().graph_walk_frequency);
    system_node()->OnMemoryPressureForTesting(
        base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
  }
}

// This test validates that we won't swap a node when the available swap space
// is below the limit.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenDiskSpaceTooLow) {
  // We will only swap a renderer once every 3 graph walks.
  policy()->config().graph_walk_frequency = base::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(1);

  policy()->config().minimum_swap_disk_space_available = 1 << 30;  // 1 GB

  EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
      .WillOnce(Return(true));
  AttachProcess();

  // We will have our mock return that there is only 100MB of disk space
  // available. This should prevent swapping because it's below the minimum
  // value.
  EXPECT_CALL(*policy(), GetSwapDeviceFreeSpaceBytes())
      .WillOnce(Return(100 << 20));  // 100MB

  // Because GetSwapDeviceFreeSpaceBytes is less than the configured minimum it
  // should return false.
  EXPECT_FALSE(
      policy()->DefaultIsEligibleToSwap(process_node().get(), nullptr));
}

// This test will validate that we won't swap a renderer that is already
// exceeding the individual renderer limit.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenPerRendererSwapExceeded) {
  // We will only swap a renderer once every 3 graph walks.
  policy()->config().graph_walk_frequency = base::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(1);

  policy()->config().renderer_maximum_disk_swap_file_size_bytes =
      128 << 20;  // 128MB

  EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
      .WillOnce(Return(true));
  AttachProcess();

  // We will have our mock return that there is only 100MB of disk space
  // available. This should prevent swapping because it's below the minimum
  // value.
  EXPECT_CALL(*policy(), GetProcessNodeSwapFileUsageBytes(process_node().get()))
      .WillOnce(Return(190 << 20));  // 190 MB

  // We're already using 190 MB which is more than the configured 128 MB so it
  // should not be eligible to swap.
  EXPECT_FALSE(
      policy()->DefaultIsEligibleToSwap(process_node().get(), nullptr));
}

// This test will validate that we don't swap renderers when we've exceeded the
// global limit.
TEST_F(UserspaceSwapPolicyTest, DontSwapWhenTotalRendererSwapExceeded) {
  // We will only swap a renderer once every 3 graph walks.
  policy()->config().graph_walk_frequency = base::Seconds(1);
  policy()->config().process_swap_frequency = base::Seconds(1);

  policy()->config().maximum_swap_disk_space_bytes = 1 << 30;  // 1 GB

  EXPECT_CALL(*policy(), InitializeProcessNode(process_node().get()))
      .WillOnce(Return(true));
  AttachProcess();

  // We will have our mock return that there is only 100MB of disk space
  // available. This should prevent swapping because it's below the minimum
  // value.
  EXPECT_CALL(*policy(), GetTotalSwapFileUsageBytes())
      .WillOnce(Return(500 << 20))    // 500 MB
      .WillOnce(Return(1200 << 20));  // 1.2 GB
  // Since we're now below the 1GB limit we expect it will succeed.
  EXPECT_TRUE(policy()->DefaultIsEligibleToSwap(process_node().get(), nullptr));

  // And on the second call we will expect that we will no longer be eligible
  // because we've exceeded 1GB.
  EXPECT_FALSE(
      policy()->DefaultIsEligibleToSwap(process_node().get(), nullptr));
}

}  // namespace
}  // namespace policies
}  // namespace performance_manager