blob: 59f05c9367ba74e3c31022c1150f7338b54de655 [file] [log] [blame]
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Author: jmarantz@google.com (Joshua Marantz)
#include "pagespeed/kernel/base/waveform.h"
#include "base/logging.h"
#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/basictypes.h" // for int64
#include "pagespeed/kernel/base/md5_hasher.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/base/writer.h"
namespace net_instaweb {
class MessageHandler;
Waveform::Waveform(ThreadSystem* thread_system, Timer* timer, int capacity,
UpDownCounter* metric)
: timer_(timer),
capacity_(capacity),
samples_(new TimeValue[capacity]),
previous_value_(0.0),
mutex_(thread_system->NewMutex()),
metric_(metric) {
// Note that we don't clear previous_value_ in Clear() because that would
// get the Waveform out of sync with whatever system is sending it Delta
// updates.
Clear();
}
void Waveform::Clear() {
ScopedMutex lock(mutex_.get());
start_index_ = 0;
size_ = 0;
first_sample_timestamp_us_ = 0;
total_since_clear_ = 0.0;
min_ = 0.0;
max_ = 0.0;
}
int Waveform::Size() {
// TODO(jmarantz): use reader-lock.
ScopedMutex lock(mutex_.get());
return size_;
}
double Waveform::Average() {
// TODO(jmarantz): use reader-lock.
ScopedMutex lock(mutex_.get());
if (size_ == 0) {
return 0.0;
}
// We could make the average by looking at the delta from
// timer_->NowUs(), rather than prev->first. But with an active
// server this won't make much difference, and with a server being
// debugged I think it's better like this: the time between the
// first event and the last event.
TimeValue* prev = GetSample(size_ - 1);
int64 elapsed_us = prev->first - first_sample_timestamp_us_;
return (total_since_clear_ / elapsed_us);
}
double Waveform::Minimum() {
// TODO(jmarantz): use reader-lock.
ScopedMutex lock(mutex_.get());
return min_;
}
double Waveform::Maximum() {
// TODO(jmarantz): use reader-lock.
ScopedMutex lock(mutex_.get());
return max_;
}
Waveform::TimeValue* Waveform::GetSample(int index) {
// Must be called with mutex held.
DCHECK_LE(0, index);
DCHECK_GT(size_, index);
return &samples_[(start_index_ + index) % capacity_];
}
void Waveform::AddDelta(double delta) {
// TODO(jmarantz): use writer-lock.
ScopedMutex lock(mutex_.get());
AddHelper(previous_value_ + delta);
if (metric_ != NULL) {
metric_->Add(static_cast<int64>(delta));
}
}
void Waveform::Add(double value) {
// TODO(jmarantz): use writer-lock.
ScopedMutex lock(mutex_.get());
AddHelper(value);
if (metric_ != NULL) {
metric_->Set(static_cast<int64>(value));
}
}
void Waveform::AddHelper(double value) {
previous_value_ = value;
int64 now_us = timer_->NowUs();
if (size_ == 0) {
max_ = value;
min_ = value;
first_sample_timestamp_us_ = now_us;
} else {
TimeValue* prev = GetSample(size_ - 1);
int64 elapsed_us = now_us - prev->first;
total_since_clear_ += elapsed_us * prev->second; // time-weighted values
if (value < min_) {
min_ = value;
} else if (value > max_) {
max_ = value;
}
}
if (size_ == capacity_) {
start_index_ = (start_index_ + 1) % capacity_;
} else {
++size_;
}
TimeValue* tv = GetSample(size_ - 1);
tv->first = now_us;
tv->second = value;
}
// See http://code.google.com/apis/chart/interactive/docs/gallery/linechart.html
namespace {
const char kChartApiLoad[] =
"<script type='text/javascript' src='https://www.google.com/jsapi'>"
"</script>\n"
"<script type='text/javascript'>\n"
" google.load('visualization', '1', {packages:['corechart']});\n"
" google.setOnLoadCallback(drawWaveforms);\n"
" var google_waveforms = new Array();\n"
" function drawWaveform(title, id, legend, points) {\n"
" var data = new google.visualization.DataTable();\n"
" data.addColumn('number', 'Time (ms)');\n"
" data.addColumn('number', legend);\n"
" data.addRows(points.length);\n"
" var min_x = 0;\n"
" var max_x = 0;\n"
" var min_y = 0;\n"
" var max_y = 0;\n"
" for (var i = 0; i < points.length; ++i) {\n"
" var point = points[i];\n"
" var x = point[0];\n"
" var y = point[1];\n"
" if ((i == 0) || (x < min_x)) { min_x = x; }\n"
" if ((i == 0) || (x > max_x)) { max_x = x; }\n"
" if ((i == 0) || (y < min_y)) { min_y = y; }\n"
" if ((i == 0) || (y > max_y)) { max_y = y; }\n"
" data.setValue(i, 0, x);\n"
" data.setValue(i, 1, y);\n"
" }\n"
" var chart = new google.visualization.ScatterChart(\n"
" document.getElementById(id));\n"
" chart.draw(data, {\n"
" lineWidth: 1,\n"
" pointSize: 3,\n"
" width: 800, height: 480, title: title, legend: 'none',\n"
" hAxis: {title: 'time (ms)', minValue: min_x, "
"maxValue: 1.1 * max_x},\n"
" vAxis: {minValue: min_y, maxValue: 1.1 * "
"max_y}});\n"
" }\n"
" function drawWaveforms() {\n"
" for (var i = 0; i < google_waveforms.length; ++i) {\n"
" var w = google_waveforms[i];\n"
" w();\n"
" }\n"
" }\n"
" function addWaveform(title, id, legend, points) {\n"
" google_waveforms.push(function() {drawWaveform(title, id, legend, "
"points);});\n"
" }\n"
"</script>";
const char kChartWaveformPrefixFormat[] =
"<script type='text/javascript'>\n"
" addWaveform('%s', '%s', '%s', [\n"; // title, id, legend
const char kSampleFormat[] =
" [%f, %f],\n";
const char kWaveformSuffixFormat[] =
"]);\n"
"</script>\n"
"<div id='%s'></div>\n";
} // namespace
void Waveform::RenderHeader(Writer* writer, MessageHandler* handler) {
writer->Write(kChartApiLoad, handler);
}
void Waveform::Render(const StringPiece& title, const StringPiece& label,
Writer* writer, MessageHandler* handler) {
ScopedMutex lock(mutex_.get());
if (size_ == 0) {
writer->Write(StrCat(title, ": no data"), handler);
} else {
TimeValue* tv = GetSample(0);
int64 start_time_us = tv->first;
MD5Hasher hasher;
GoogleString div_id = hasher.Hash(title);
writer->Write(StringPrintf(kChartWaveformPrefixFormat,
title.as_string().c_str(),
div_id.c_str(),
label.as_string().c_str()),
handler);
for (int i = 0; i < size_; ++i) {
tv = GetSample(i);
int64 delta_us = tv->first - start_time_us;
writer->Write(StringPrintf(kSampleFormat, delta_us / 1000.0,
static_cast<double>(tv->second)),
handler);
}
writer->Write(StringPrintf(kWaveformSuffixFormat, div_id.c_str()),
handler);
}
}
} // namespace net_instaweb