Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f1e85e49 authored by Darin Petkov's avatar Darin Petkov
Browse files

Implement a persistent storage aggregation counter class.

This class is currently used to aggregate the active daily use time
but can also be used to aggregate other data (e.g., active use time
between crashes) before sending to UMA. Abstracting this in a separate
class also simplifies the daemon unit tests.

An alternative design would store the data on shutdown (but may slow
down shutdown a little). This should do it for now.

BUG=none
TEST=gmerged on device,inspected logs,about:histograms,etc.

Review URL: http://codereview.chromium.org/2731008
parent 99c64a0d
Loading
Loading
Loading
Loading
+17 −7
Original line number Diff line number Diff line
@@ -16,32 +16,42 @@ DAEMON_TEST = metrics_daemon_test
LIB = libmetrics.a
SHAREDLIB = libmetrics.so
LIB_TEST = metrics_library_test
COUNTER_TEST = counter_test

TESTCOUNTER_OBJS = \
	counter.o \
	counter_test.o
CLIENT_OBJS = \
	metrics_client.o
LIB_OBJS = \
	metrics_library.o
TESTLIB_OBJS = \
	metrics_library.o \
	metrics_library_test.o
DAEMON_OBJS = \
	counter.o \
	metrics_daemon.o \
	metrics_daemon_main.o
TESTDAEMON_OBJS = \
	counter.o \
	metrics_daemon.o \
	metrics_daemon_test.o
LIB_OBJS = \
	metrics_library.o
TESTLIB_OBJS = \
	metrics_library.o \
	metrics_library_test.o

TESTCOUNTER_LIBS = -lgmock -lgtest -lbase -lrt -lpthread
DAEMON_LDFLAGS = $(LDFLAGS) $(LDCONFIG) -lrt -lbase -lpthread -lgflags
TESTDAEMON_LIBS = -lgmock -lgtest
TESTLIB_LIBS = -lgtest -lbase -lrt -lpthread

all: $(LIB) $(SHAREDLIB) $(CLIENT) $(DAEMON)

tests: $(DAEMON_TEST) $(LIB_TEST)
tests: $(COUNTER_TEST) $(DAEMON_TEST) $(LIB_TEST)

$(CLIENT): $(CLIENT_OBJS) $(SHAREDLIB)
	$(CXX) $(LDFLAGS) $^ -o $@

$(COUNTER_TEST): $(TESTCOUNTER_OBJS)
	$(CXX) -o $@ $^ $(TESTCOUNTER_LIBS)

$(DAEMON): $(DAEMON_OBJS) $(SHAREDLIB)
	$(CXX) -o $@ $^ $(DAEMON_LDFLAGS)

@@ -73,4 +83,4 @@ metrics_daemon_test.o: \

clean:
	rm -f $(CLIENT) $(DAEMON) $(LIB) $(SHAREDLIB) *.o
	rm -f $(DAEMON_TEST) $(LIB_TEST)
	rm -f $(COUNTER_TEST) $(DAEMON_TEST) $(LIB_TEST)

metrics/counter.cc

0 → 100644
+176 −0
Original line number Diff line number Diff line
// Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "counter.h"

#include <sys/file.h>

#include <base/eintr_wrapper.h>
#include <base/logging.h>

namespace chromeos_metrics {

// TaggedCounter::Record implementation.
void TaggedCounter::Record::Init(int tag, int count) {
  tag_ = tag;
  count_ = (count > 0) ? count : 0;
}

void TaggedCounter::Record::Add(int count) {
  if (count <= 0)
    return;

  count_ += count;

  // Saturates on postive overflow.
  if (count_ < 0) {
    count_ = INT_MAX;
  }
}

// TaggedCounter implementation.
TaggedCounter::TaggedCounter()
    : filename_(NULL), reporter_(NULL), reporter_handle_(NULL),
      record_state_(kRecordInvalid) {}

TaggedCounter::~TaggedCounter() {}

void TaggedCounter::Init(const char* filename,
                         Reporter reporter, void* reporter_handle) {
  DCHECK(filename);
  filename_ = filename;
  reporter_ = reporter;
  reporter_handle_ = reporter_handle;
  record_state_ = kRecordInvalid;
}

void TaggedCounter::Update(int tag, int count) {
  UpdateInternal(tag,
                 count,
                 false);  // No flush.
}

void TaggedCounter::Flush() {
  UpdateInternal(0,  // tag
                 0,  // count
                 true);  // Do flush.
}

void TaggedCounter::UpdateInternal(int tag, int count, bool flush) {
  // If there's no new data and the last record in the aggregation
  // file is with the same tag, there's nothing to do.
  if (!flush && count <= 0 &&
      record_state_ == kRecordValid && record_.tag() == tag)
    return;

  DLOG(INFO) << "tag: " << tag << " count: " << count << " flush: " << flush;
  DCHECK(filename_);

  // NOTE: The assumption is that this TaggedCounter object is the
  // sole owner of the persistent storage file so no locking is
  // necessary.
  int fd = HANDLE_EINTR(open(filename_, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR));
  if (fd < 0) {
    PLOG(WARNING) << "Unable to open the persistent counter file";
    return;
  }

  ReadRecord(fd);
  ReportRecord(tag, flush);
  UpdateRecord(tag, count);
  WriteRecord(fd);

  HANDLE_EINTR(close(fd));
}

void TaggedCounter::ReadRecord(int fd) {
  if (record_state_ != kRecordInvalid)
    return;

  if (HANDLE_EINTR(read(fd, &record_, sizeof(record_))) == sizeof(record_)) {
    if (record_.count() > 0) {
      record_state_ = kRecordValid;
      return;
    }
    // This shouldn't happen normally unless somebody messed with the
    // persistent storage file.
    NOTREACHED();
    record_state_ = kRecordNullDirty;
    return;
  }

  record_state_ = kRecordNull;
}

void TaggedCounter::ReportRecord(int tag, bool flush) {
  // If no valid record, there's nothing to report.
  if (record_state_ != kRecordValid) {
    DCHECK(record_state_ == kRecordNull);
    return;
  }

  // If the current record has the same tag as the new tag, it's not
  // ready to be reported yet.
  if (!flush && record_.tag() == tag)
    return;

  if (reporter_) {
    reporter_(reporter_handle_, record_.tag(), record_.count());
  }
  record_state_ = kRecordNullDirty;
}

void TaggedCounter::UpdateRecord(int tag, int count) {
  if (count <= 0)
    return;

  switch (record_state_) {
    case kRecordNull:
    case kRecordNullDirty:
      // Current record is null, starting a new record.
      record_.Init(tag, count);
      record_state_ = kRecordValidDirty;
      break;

    case kRecordValid:
      // If there's an existing record for the current tag,
      // accumulates the counts.
      DCHECK_EQ(record_.tag(), tag);
      record_.Add(count);
      record_state_ = kRecordValidDirty;
      break;

    default:
      NOTREACHED();
  }
}

void TaggedCounter::WriteRecord(int fd) {
  switch (record_state_) {
    case kRecordNullDirty:
      // Truncates the aggregation file to discard the record.
      PLOG_IF(WARNING, HANDLE_EINTR(ftruncate(fd, 0)) != 0);
      record_state_ = kRecordNull;
      break;

    case kRecordValidDirty:
      // Updates the accumulator record in the file if there's new data.
      PLOG_IF(WARNING, HANDLE_EINTR(lseek(fd, 0, SEEK_SET)) != 0);
      PLOG_IF(WARNING,
              HANDLE_EINTR(write(fd, &record_, sizeof(record_))) !=
              sizeof(record_));
      record_state_ = kRecordValid;
      break;

    case kRecordNull:
    case kRecordValid:
      // Nothing to do.
      break;

    default:
      NOTREACHED();
  }
}

}  // namespace chromeos_metrics

metrics/counter.h

0 → 100644
+151 −0
Original line number Diff line number Diff line
// Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef METRICS_COUNTER_H_
#define METRICS_COUNTER_H_

#include <gtest/gtest_prod.h>  // for FRIEND_TEST

namespace chromeos_metrics {

// TaggedCounter maintains a persistent storage (i.e., a file)
// aggregation counter for a given tag (e.g., day, hour) that survives
// system shutdowns, reboots and crashes, as well as daemon process
// restarts. The counter object is initialized by pointing to the
// persistent storage file and providing a callback used for reporting
// aggregated data.  The counter can then be updated with additional
// event counts.  The aggregated count is reported through the
// callback when the counter is explicitly flushed or when data for a
// new tag arrives.
class TaggedCounterInterface {
 public:
  // Callback type used for reporting aggregated or flushed data.
  // Once this callback is invoked by the counter, the reported
  // aggregated data is discarded. Only aggregated data with positive
  // counts is reported.
  //
  // |handle| is the |reporter_handle| pointer passed through Init.
  // |tag| is the tag associated with the aggregated count.
  // |count| is aggregated count.
  typedef void (*Reporter)(void* handle, int tag, int count);

  virtual ~TaggedCounterInterface() {}

  // Initializes the counter by providing the persistent storage
  // location |filename| and a |reporter| callback for reporting
  // aggregated counts. |reporter_handle| is sent to the |reporter|
  // along with the aggregated counts.
  //
  // NOTE: The assumption is that this object is the sole owner of the
  // persistent storage file so no locking is currently implemented.
  virtual void Init(const char* filename,
                    Reporter reporter, void* reporter_handle) = 0;

  // Adds |count| of events for the given |tag|. If there's an
  // existing aggregated count for a different tag, it's reported
  // through the reporter callback and discarded.
  virtual void Update(int tag, int count) = 0;

  // Reports the current aggregated count (if any) through the
  // reporter callback and discards it.
  virtual void Flush() = 0;
};

class TaggedCounter : public TaggedCounterInterface {
 public:
  TaggedCounter();
  ~TaggedCounter();

  // Implementation of interface methods.
  void Init(const char* filename, Reporter reporter, void* reporter_handle);
  void Update(int tag, int count);
  void Flush();

 private:
  friend class RecordTest;
  friend class TaggedCounterTest;
  FRIEND_TEST(TaggedCounterTest, BadFileLocation);
  FRIEND_TEST(TaggedCounterTest, Flush);
  FRIEND_TEST(TaggedCounterTest, InitFromFile);
  FRIEND_TEST(TaggedCounterTest, Update);

  // The current tag/count record is cached by the counter object to
  // avoid potentially unnecessary I/O. The cached record can be in
  // one of the following states:
  enum RecordState {
    kRecordInvalid,    // Invalid record, sync from persistent storage needed.
    kRecordNull,       // No current record, persistent storage synced.
    kRecordNullDirty,  // No current record, persistent storage is invalid.
    kRecordValid,      // Current record valid, persistent storage synced.
    kRecordValidDirty  // Current record valid, persistent storage is invalid.
  };

  // Defines the tag/count record. Objects of this class are synced
  // with the persistent storage through binary reads/writes.
  class Record {
   public:
    // Creates a new Record with |tag_| and |count_| reset to 0.
    Record() : tag_(0), count_(0) {}

    // Initializes with |tag| and |count|. If |count| is negative,
    // |count_| is set to 0.
    void Init(int tag, int count);

    // Adds |count| to the current |count_|. Negative |count| is
    // ignored. In case of positive overflow, |count_| is saturated to
    // INT_MAX.
    void Add(int count);

    int tag() const { return tag_; }
    int count() const { return count_; }

   private:
    int tag_;
    int count_;
  };

  // Implementation of the Update and Flush methods. Goes through the
  // necessary steps to read, report, update, and sync the aggregated
  // record.
  void UpdateInternal(int tag, int count, bool flush);

  // If the current cached record is invalid, reads it from persistent
  // storage specified through file descriptor |fd| and updates the
  // cached record state to either null, or valid depending on the
  // persistent storage contents.
  void ReadRecord(int fd);

  // If there's an existing valid record and either |flush| is true,
  // or the new |tag| is different than the old one, reports the
  // aggregated data through the reporter callback and resets the
  // cached record.
  void ReportRecord(int tag, bool flush);

  // Updates the cached record given the new |tag| and |count|. This
  // method expects either a null cached record, or a valid cached
  // record with the same tag as |tag|.
  void UpdateRecord(int tag, int count);

  // If the cached record state is dirty, updates the persistent
  // storage specified through file descriptor |fd| and switches the
  // record state to non-dirty.
  void WriteRecord(int fd);

  // Persistent storage file path.
  const char* filename_;

  // Aggregated data reporter callback and handle to pass-through.
  Reporter reporter_;
  void* reporter_handle_;

  // Current cached aggregation record.
  Record record_;

  // Current cached aggregation record state.
  RecordState record_state_;
};

}  // namespace chromeos_metrics

#endif  // METRICS_COUNTER_H_

metrics/counter_mock.h

0 → 100644
+26 −0
Original line number Diff line number Diff line
// Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef METRICS_COUNTER_MOCK_H_
#define METRICS_COUNTER_MOCK_H_

#include <string>

#include <gmock/gmock.h>

#include "counter.h"

namespace chromeos_metrics {

class TaggedCounterMock : public TaggedCounterInterface {
 public:
  MOCK_METHOD3(Init, void(const char* filename,
                          Reporter reporter, void* reporter_handle));
  MOCK_METHOD2(Update, void(int tag, int count));
  MOCK_METHOD0(Flush, void());
};

}  // namespace chromeos_metrics

#endif  // METRICS_COUNTER_MOCK_H_
+262 −0
Original line number Diff line number Diff line
// Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <sys/file.h>

#include <base/eintr_wrapper.h>
#include <base/file_util.h>
#include <base/logging.h>
#include <base/string_util.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include "counter.h"

using ::testing::_;
using ::testing::MockFunction;
using ::testing::StrictMock;

namespace chromeos_metrics {

static const char kTestRecordFile[] = "record-file";
static const char kDoesNotExistFile[] = "/does/not/exist";

class RecordTest : public testing::Test {
 protected:
  virtual void SetUp() {
    EXPECT_EQ(0, record_.tag());
    EXPECT_EQ(0, record_.count());
  }

  // The record under test.
  TaggedCounter::Record record_;
};

class TaggedCounterTest : public testing::Test {
 protected:
  virtual void SetUp() {
    EXPECT_EQ(NULL, counter_.filename_);
    EXPECT_TRUE(NULL == counter_.reporter_);
    EXPECT_EQ(NULL, counter_.reporter_handle_);
    EXPECT_EQ(TaggedCounter::kRecordInvalid, counter_.record_state_);

    counter_.Init(kTestRecordFile, &Reporter, this);
    EXPECT_TRUE(AssertNoOrEmptyRecordFile());
    EXPECT_EQ(kTestRecordFile, counter_.filename_);
    EXPECT_TRUE(Reporter == counter_.reporter_);
    EXPECT_EQ(this, counter_.reporter_handle_);
    EXPECT_EQ(TaggedCounter::kRecordInvalid, counter_.record_state_);

    // The test fixture object will be used by the log message handler.
    test_ = this;
    logging::SetLogMessageHandler(HandleLogMessages);
  }

  virtual void TearDown() {
    logging::SetLogMessageHandler(NULL);
    test_ = NULL;
    file_util::Delete(FilePath(kTestRecordFile), false);
  }

  // Asserts that the record file contains the specified contents.
  testing::AssertionResult AssertRecord(const char* expr_tag,
                                        const char* expr_count,
                                        int expected_tag,
                                        int expected_count) {
    int fd = HANDLE_EINTR(open(kTestRecordFile, O_RDONLY));
    if (fd < 0) {
      testing::Message msg;
      msg << "Unable to open " << kTestRecordFile;
      return testing::AssertionFailure(msg);
    }

    TaggedCounter::Record record;
    if (!file_util::ReadFromFD(fd, reinterpret_cast<char*>(&record),
                               sizeof(record))) {
      testing::Message msg;
      msg << "Unable to read " << sizeof(record) << " bytes from "
          << kTestRecordFile;
      HANDLE_EINTR(close(fd));
      return testing::AssertionFailure(msg);
    }

    if (record.tag() != expected_tag || record.count() != expected_count) {
      testing::Message msg;
      msg << "actual record (" << record.tag() << ", " << record.count()
          << ") expected (" << expected_tag << ", " << expected_count << ")";
      HANDLE_EINTR(close(fd));
      return testing::AssertionFailure(msg);
    }

    HANDLE_EINTR(close(fd));
    return testing::AssertionSuccess();
  }

  // Returns true if the persistent record file does not exist or is
  // empty, false otherwise.
  bool AssertNoOrEmptyRecordFile() {
    FilePath record_file(counter_.filename_);
    int64 record_file_size;
    return !file_util::PathExists(record_file) ||
        (file_util::GetFileSize(record_file, &record_file_size) &&
         record_file_size == 0);
  }

  // Adds a reporter call expectation that the specified tag/count
  // callback will be generated.
  void ExpectReporterCall(int tag, int count) {
    EXPECT_CALL(reporter_, Call(_, tag, count))
        .Times(1)
        .RetiresOnSaturation();
  }

  // The reporter callback forwards the call to the reporter mock so
  // that we can set call expectations.
  static void Reporter(void* handle, int tag, int count) {
    TaggedCounterTest* test = static_cast<TaggedCounterTest*>(handle);
    ASSERT_FALSE(NULL == test);
    test->reporter_.Call(handle, tag, count);
  }

  // Collects log messages in the |log_| member string so that they
  // can be analyzed for errors and expected behavior.
  static bool HandleLogMessages(int severity, const std::string& str) {
    test_->log_.append(str);
    test_->log_.append("\n");

    // Returning true would mute the log.
    return false;
  }

  // Returns true if the counter log contains |pattern|, false otherwise.
  bool LogContains(const std::string& pattern) {
    return log_.find(pattern) != std::string::npos;
  }

  // The TaggedCounter object under test.
  TaggedCounter counter_;

  // The accumulated counter log.
  std::string log_;

  // Reporter mock to set callback expectations on.
  StrictMock<MockFunction<void(void* handle, int tag, int count)> > reporter_;

  // Pointer to the current test fixture.
  static TaggedCounterTest* test_;
};

// static
TaggedCounterTest* TaggedCounterTest::test_ = NULL;

TEST_F(RecordTest, Init) {
  record_.Init(/* tag */ 5, /* count */ -1);
  EXPECT_EQ(5, record_.tag());
  EXPECT_EQ(0, record_.count());

  record_.Init(/* tag */ -2, /* count */ 10);
  EXPECT_EQ(-2, record_.tag());
  EXPECT_EQ(10, record_.count());
}

TEST_F(RecordTest, Add) {
  record_.Add(/* count */ -1);
  EXPECT_EQ(0, record_.count());

  record_.Add(/* count */ 5);
  EXPECT_EQ(5, record_.count());

  record_.Add(/* count */ 10);
  EXPECT_EQ(15, record_.count());

  record_.Add(/* count */ -2);
  EXPECT_EQ(15, record_.count());

  record_.Add(/* count */ INT_MAX);
  EXPECT_EQ(INT_MAX, record_.count());

  record_.Add(/* count */ 1);
  EXPECT_EQ(INT_MAX, record_.count());
}

TEST_F(TaggedCounterTest, BadFileLocation) {
  // Checks that the counter doesn't die badly if the file can't be
  // created.
  counter_.Init(kDoesNotExistFile,
                /* reporter */ NULL, /* reporter_handle */ NULL);
  counter_.Update(/* tag */ 10, /* count */ 20);
  EXPECT_TRUE(LogContains("Unable to open the persistent counter file: "
                          "No such file or directory"));
  EXPECT_EQ(TaggedCounter::kRecordInvalid, counter_.record_state_);
  file_util::Delete(FilePath(kDoesNotExistFile), false);
}

TEST_F(TaggedCounterTest, Flush) {
  counter_.Flush();
  EXPECT_EQ(TaggedCounter::kRecordNull, counter_.record_state_);

  counter_.Update(/* tag */ 40, /* count */ 60);
  ExpectReporterCall(/* tag */ 40, /* count */ 60);
  counter_.Flush();
  EXPECT_TRUE(AssertNoOrEmptyRecordFile());
  EXPECT_EQ(TaggedCounter::kRecordNull, counter_.record_state_);

  counter_.Update(/* tag */ 41, /* count */ 70);
  counter_.record_.Init(/* tag */ 0, /* count */ 0);
  counter_.record_state_ = TaggedCounter::kRecordInvalid;
  ExpectReporterCall(/* tag */ 41, /* count */ 70);
  counter_.Flush();
  EXPECT_TRUE(AssertNoOrEmptyRecordFile());
  EXPECT_EQ(TaggedCounter::kRecordNull, counter_.record_state_);
}

TEST_F(TaggedCounterTest, InitFromFile) {
  counter_.Update(/* tag */ 30, /* count */ 50);
  EXPECT_PRED_FORMAT2(AssertRecord, /* day */ 30, /* seconds */ 50);
  EXPECT_EQ(TaggedCounter::kRecordValid, counter_.record_state_);

  counter_.Init(kTestRecordFile, &Reporter, this);
  counter_.Update(/* tag */ 30, /* count */ 40);
  EXPECT_PRED_FORMAT2(AssertRecord, /* day */ 30, /* seconds */ 90);
  EXPECT_EQ(TaggedCounter::kRecordValid, counter_.record_state_);

  counter_.Init(kTestRecordFile, &Reporter, this);
  ExpectReporterCall(/* tag */ 30, /* count */ 90);
  counter_.Update(/* tag */ 31, /* count */ 60);
  EXPECT_PRED_FORMAT2(AssertRecord, /* day */ 31, /* seconds */ 60);
  EXPECT_EQ(TaggedCounter::kRecordValid, counter_.record_state_);

  ExpectReporterCall(/* tag */ 31, /* count */ 60);
  counter_.Init(kTestRecordFile, &Reporter, this);
  counter_.Update(/* tag */ 32, /* count */ 0);
  EXPECT_TRUE(AssertNoOrEmptyRecordFile());
  EXPECT_EQ(TaggedCounter::kRecordNull, counter_.record_state_);
}

TEST_F(TaggedCounterTest, Update) {
  counter_.Update(/* tag */ 20, /* count */ 30);
  EXPECT_PRED_FORMAT2(AssertRecord, /* day */ 20, /* seconds */ 30);
  EXPECT_EQ(TaggedCounter::kRecordValid, counter_.record_state_);

  counter_.Update(/* tag */ 20, /* count */ 40);
  EXPECT_PRED_FORMAT2(AssertRecord, /* day */ 20, /* seconds */ 70);
  EXPECT_EQ(TaggedCounter::kRecordValid, counter_.record_state_);

  ExpectReporterCall(/* tag */ 20, /* count */ 70);
  counter_.Update(/* tag */ 21, /* count */ 15);
  EXPECT_PRED_FORMAT2(AssertRecord, /* day */ 21, /* seconds */ 15);
  EXPECT_EQ(TaggedCounter::kRecordValid, counter_.record_state_);

  ExpectReporterCall(/* tag */ 21, /* count */ 15);
  counter_.Update(/* tag */ 22, /* count */ 0);
  EXPECT_TRUE(AssertNoOrEmptyRecordFile());
  EXPECT_EQ(TaggedCounter::kRecordNull, counter_.record_state_);
}

}  // namespace chromeos_metrics

int main(int argc, char** argv) {
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}
Loading