HCE Project DC service web UI  0.2
Hierarchical Cluster Engine DC service web UI
 All Classes Namespaces Files Functions Variables Pages
Yii2DbPanel.php
Go to the documentation of this file.
1 <?php
2 
9 {
13  public $insertParamValues = true;
17  public $canExplain = true;
18 
22  public function init()
23  {
24  $this->_logsEnabled = true;
25  $this->_logsLevels = CLogger::LEVEL_PROFILE;
26  $this->_logsCategories = 'system.db.CDbCommand.*';
27  parent::init();
28  }
29 
30  public function getName()
31  {
32  return 'Database';
33  }
34 
35  public function getSummary()
36  {
37  $timings = $this->calculateTimings();
38  $count = count($timings);
39  $time = 0;
40  foreach ($timings as $timing) {
41  $time += $timing[4];
42  }
43  if (!$count) return '';
44  return $this->render(dirname(__FILE__) . '/../views/panels/db_bar.php', array(
45  'count' => $count,
46  'time' => number_format($time * 1000) . ' ms',
47  ));
48  }
49 
50  public function getDetail()
51  {
52  return $this->render(dirname(__FILE__) . '/../views/panels/db.php', array(
53  'queries' => $this->getQueriesInfo(),
54  'queriesCount' => count($this->calculateTimings()),
55  'resume' => $this->getResumeInfo(),
56  'resumeCount' => count($this->calculateResume()),
57  'connectionsCount' => count($this->data['connections']),
58  'connections' => $this->getConnectionsInfo(),
59  ));
60  }
61 
65  protected function getQueriesInfo()
66  {
67  $items = array();
68  foreach ($this->calculateTimings() as $timing) {
69  $items[] = array(
70  'time' => date('H:i:s.', $timing[3]) . sprintf('%03d', (int)(($timing[3] - (int)$timing[3]) * 1000)),
71  'duration' => sprintf('%.1f ms', $timing[4] * 1000),
72  'procedure' => $this->formatSql($timing[1], $this->insertParamValues),
73  );
74  }
75  return $items;
76  }
77 
81  protected function getResumeInfo()
82  {
83  $items = array();
84  foreach ($this->calculateResume() as $item) {
85  $items[] = array(
86  'procedure' => $item[0],
87  'count' => $item[1],
88  'total' => sprintf('%.1f ms', $item[2] * 1000),
89  'avg' => sprintf('%.1f ms', $item[2] * 1000 / $item[1]),
90  'min' => sprintf('%.1f ms', $item[3] * 1000),
91  'max' => sprintf('%.1f ms', $item[4] * 1000),
92  );
93  }
94  return $items;
95  }
96 
100  protected function getConnectionsInfo()
101  {
102  $connections = array();
103  foreach ($this->data['connections'] as $id => $connection) {
104  if ($connection['driver'] == 'mysql' && isset($connection['info'])) {
105  foreach (explode(' ', $connection['info']) as $line) {
106  list($key, $value) = explode(': ', $line, 2);
107  $connection[$key] = $value;
108  }
109  unset($connection['info']);
110  }
111  $connections[$id] = $connection;
112  }
113  return $connections;
114  }
115 
116  private $_timings;
117 
122  protected function calculateTimings()
123  {
124  if ($this->_timings !== null) {
125  return $this->_timings;
126  }
127  $messages = $this->data['messages'];
128  $timings = array();
129  $stack = array();
130  foreach ($messages as $i => $log) {
131  list($token, , $category, $timestamp) = $log;
132  $log[4] = $i;
133  if (strpos($token, 'begin:') === 0) {
134  $log[0] = $token = substr($token, 6);
135  $stack[] = $log;
136  } elseif (strpos($token, 'end:') === 0) {
137  $log[0] = $token = substr($token, 4);
138  if (($last = array_pop($stack)) !== null && $last[0] === $token) {
139  $timings[$last[4]] = array(count($stack), $token, $category, $last[3], $timestamp - $last[3]);
140  }
141  }
142  }
143  $now = microtime(true);
144  while (($last = array_pop($stack)) !== null) {
145  $delta = $now - $last[3];
146  $timings[$last[4]] = array(count($stack), $last[0], $last[2], $last[3], $delta);
147  }
148  ksort($timings);
149  return $this->_timings = $timings;
150  }
151 
152  private $_resume;
153 
158  protected function calculateResume()
159  {
160  if ($this->_resume !== null) {
161  return $this->_resume;
162  }
163  $resume = array();
164  foreach ($this->calculateTimings() as $timing) {
165  $duration = $timing[4];
166  $query = $this->formatSql($timing[1], $this->insertParamValues);
167  $key = md5($query);
168  if (!isset($resume[$key])) {
169  $resume[$key] = array($query, 1, $duration, $duration, $duration);
170  } else {
171  $resume[$key][1]++;
172  $resume[$key][2] += $duration;
173  if ($resume[$key][3] > $duration) $resume[$key][3] = $duration;
174  if ($resume[$key][4] < $duration) $resume[$key][4] = $duration;
175  }
176  }
177  usort($resume, array($this, 'compareResume'));
178  return $this->_resume = $resume;
179  }
180 
181  private function compareResume($a, $b)
182  {
183  if ($a[2] == $b[2]) return 0;
184  return $a[2] < $b[2] ? 1 : -1;
185  }
186 
193  public function formatSql($message, $insertParams)
194  {
195  $sqlStart = strpos($message, '(') + 1;
196  $sqlEnd = strrpos($message , ')');
197  $sql = substr($message, $sqlStart, $sqlEnd - $sqlStart);
198  if (strpos($sql, '. Bound with ') !== false) {
199  list($query, $params) = explode('. Bound with ', $sql);
200  if (!$insertParams) return $query;
201  $sql = $this->insertParamsToSql($query, $this->parseParamsSql($params));
202  }
203  return $sql;
204  }
205 
211  private function parseParamsSql($params)
212  {
213  $binds = array();
214  $pos = 0;
215  while (preg_match('/((?:\:[a-z0-9\.\_\-]+)|\d+)\s*\=\s*/i', $params, $m, PREG_OFFSET_CAPTURE, $pos)) {
216  $start = $m[0][1] + strlen($m[0][0]);
217  $key = $m[1][0];
218  if (($params{$start} == '"') || ($params{$start} == "'")) {
219  $quote = $params{$start};
220  $pos = $start;
221  while (($pos = strpos($params, $quote, $pos + 1)) !== false) {
222  $slashes = 0;
223  while ($params{$pos - $slashes - 1} == '\\') $slashes++;
224  if ($slashes % 2 == 0) {
225  $binds[$key] = substr($params, $start, $pos - $start + 1);
226  $pos++;
227  break;
228  }
229  }
230  } elseif (($end = strpos($params, ',', $start + 1)) !== false) {
231  $binds[$key] = substr($params, $start, $end - $start);
232  $pos = $end + 1;
233  } else {
234  $binds[$key] = substr($params, $start, strlen($params) - $start);
235  break;
236  }
237  }
238  return $binds;
239  }
240 
250  private function insertParamsToSql($query, $params)
251  {
252  $sql = '';
253  $pos = 0;
254  do {
255  // Выявление ближайшей заэкранированной части строки
256  $quote = '';
257  if (preg_match('/[`"\']/', $query, $m, PREG_OFFSET_CAPTURE, $pos)) {
258  $qchar = $m[0][0];
259  $qbegin = $m[0][1];
260  $qend = $qbegin;
261  do {
262  $sls = 0;
263  if (($qend = strpos($query, $qchar, $qend + 1)) !== false) {
264  while ($query{$qend - $sls - 1} == '\\') $sls++;
265  } else {
266  $qend = strlen($query) - 1;
267  }
268  } while ($sls % 2);
269  $quote = substr($query, $qbegin, $qend - $qbegin + 1);
270  $token = substr($query, $pos, $qbegin - $pos);
271  $pos = $qend + 1;
272  } else {
273  $token = substr($query, $pos);
274  }
275  // Подстановка параметров в незаэкранированную часть SQL
276  $subsql = '';
277  $pind= 0;
278  $tpos = 0;
279  while (preg_match('/\:[a-z0-9\.\_\-]+|\?/i', $token, $m, PREG_OFFSET_CAPTURE, $tpos)) {
280  $key = $m[0][0];
281  if ($key == '?') $key = $pind++;
282  if (isset($params[$key])) {
283  $value = $params[$key];
284  } else {
285  $value = $m[0][0];
286  }
287  $subsql .= substr($token, $tpos, $m[0][1] - $tpos) . $value;
288  $tpos = $m[0][1] + strlen($m[0][0]);
289  }
290  $subsql .= substr($token, $tpos);
291  // Склейка
292  $sql .= $subsql . $quote;
293  } while ($quote !== '');
294  return $sql;
295  }
296 
300  private $_hl;
301 
307  public function highlightSql($sql)
308  {
309  if ($this->_hl === null) {
310  $this->_hl = Yii::createComponent(array(
311  'class' => 'CTextHighlighter',
312  'language' => 'sql',
313  'showLineNumbers' => false,
314  ));
315  }
316  $html = $this->_hl->highlight($sql);
317  return strip_tags($html, '<div>,<span>');
318  }
319 
320  public function save()
321  {
322  $connections = array();
323  foreach (Yii::app()->getComponents() as $id => $component) {
324  if ($component instanceof CDbConnection) {
325  /* @var CDbConnection $component */
326  $connections[$id] = array(
327  'class' => get_class($component),
328  'driver' => $component->getDriverName(),
329  );
330  try {
331  $connections[$id]['server'] = $component->getServerVersion();
332  $connections[$id]['info'] = $component->getServerInfo();
333  } catch (Exception $e) {}
334  }
335  }
336 
337  return array(
338  'messages' => $this->getLogs(),
339  'connections' => $connections,
340  );
341  }
342 
349  public static function getExplainQuery($query, $driver)
350  {
351  if (preg_match('/^\s*SELECT/', $query)) {
352  switch ($driver) {
353  case 'mysql': return 'EXPLAIN ' . $query;
354  case 'pgsql': return 'EXPLAIN ' . $query;
355  case 'sqlite': return 'EXPLAIN QUERY PLAN ' . $query;
356  case 'oci': return 'EXPLAIN PLAN FOR ' . $query;
357  }
358  }
359  return null;
360  }
361 
368  public static function explain($query, $connection)
369  {
370  $procedure = self::getExplainQuery($query, $connection->driverName);
371  if ($procedure === null) {
372  throw new Exception('Explain not available');
373  }
374  switch ($connection->driverName) {
375  case 'oci':
376  $connection->createCommand($procedure)->execute();
377  return $connection->createCommand('SELECT * FROM table(dbms_xplan.display)')->queryAll();
378  default:
379  return $connection->createCommand($procedure)->queryAll();
380  }
381  }
382 
388  public function getExplainConnections($query)
389  {
390  $connections = array();
391  foreach ($this->data['connections'] as $name => $connection) {
392  if (self::getExplainQuery($query, $connection['driver']) !== null) {
393  $connections[$name] = $connection;
394  }
395  }
396  return $connections;
397  }
398 
403  public function messageByNum($number)
404  {
405  foreach ($this->calculateTimings() as $timing) {
406  if (!$number--) {
407  return $timing[1];
408  }
409  }
410  return null;
411  }
412 }