You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Profiler.java 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. /*
  2. * Copyright 2000-2016 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client;
  17. import java.util.ArrayList;
  18. import java.util.Collection;
  19. import java.util.Collections;
  20. import java.util.Comparator;
  21. import java.util.HashMap;
  22. import java.util.HashSet;
  23. import java.util.LinkedHashMap;
  24. import java.util.LinkedList;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Set;
  28. import java.util.logging.Logger;
  29. import com.google.gwt.core.client.Duration;
  30. import com.google.gwt.core.client.GWT;
  31. import com.google.gwt.core.client.JavaScriptObject;
  32. import com.google.gwt.core.client.JsArray;
  33. /**
  34. * Lightweight profiling tool that can be used to collect profiling data with
  35. * zero overhead unless enabled. To enable profiling, add
  36. * <code>&lt;set-property name="vaadin.profiler" value="true" /&gt;</code> to
  37. * your .gwt.xml file.
  38. *
  39. * @author Vaadin Ltd
  40. * @since 7.0.0
  41. */
  42. public class Profiler {
  43. private static RelativeTimeSupplier RELATIVE_TIME_SUPPLIER;
  44. private static final String evtGroup = "VaadinProfiler";
  45. private static ProfilerResultConsumer consumer;
  46. /**
  47. * Class to include using deferred binding to enable the profiling.
  48. *
  49. * @author Vaadin Ltd
  50. * @since 7.0.0
  51. */
  52. public static class EnabledProfiler extends Profiler {
  53. @Override
  54. protected boolean isImplEnabled() {
  55. return true;
  56. }
  57. }
  58. /**
  59. * Interface for getting data from the {@link Profiler}.
  60. * <p>
  61. * <b>Warning!</b> This interface is most likely to change in the future
  62. *
  63. * @since 7.1
  64. * @author Vaadin Ltd
  65. */
  66. public interface ProfilerResultConsumer {
  67. public void addProfilerData(Node rootNode, List<Node> totals);
  68. public void addBootstrapData(LinkedHashMap<String, Double> timings);
  69. }
  70. /**
  71. * A hierarchical representation of the time spent running a named block of
  72. * code.
  73. * <p>
  74. * <b>Warning!</b> This class is most likely to change in the future and is
  75. * therefore defined in this class in an internal package instead of
  76. * Profiler where it might seem more logical.
  77. */
  78. public static class Node {
  79. private final String name;
  80. private final LinkedHashMap<String, Node> children = new LinkedHashMap<>();
  81. private double time = 0;
  82. private int count = 0;
  83. private double enterTime = 0;
  84. private double minTime = 1000000000;
  85. private double maxTime = 0;
  86. /**
  87. * Create a new node with the given name.
  88. *
  89. * @param name
  90. */
  91. public Node(String name) {
  92. this.name = name;
  93. }
  94. /**
  95. * Gets the name of the node
  96. *
  97. * @return the name of the node
  98. */
  99. public String getName() {
  100. return name;
  101. }
  102. /**
  103. * Creates a new child node or retrieves and existing child and updates
  104. * its total time and hit count.
  105. *
  106. * @param name
  107. * the name of the child
  108. * @param timestamp
  109. * the timestamp for when the node is entered
  110. * @return the child node object
  111. */
  112. public Node enterChild(String name, double timestamp) {
  113. Node child = children.get(name);
  114. if (child == null) {
  115. child = new Node(name);
  116. children.put(name, child);
  117. }
  118. child.enterTime = timestamp;
  119. child.count++;
  120. return child;
  121. }
  122. /**
  123. * Gets the total time spent in this node, including time spent in sub
  124. * nodes
  125. *
  126. * @return the total time spent, in milliseconds
  127. */
  128. public double getTimeSpent() {
  129. return time;
  130. }
  131. /**
  132. * Gets the minimum time spent for one invocation of this node,
  133. * including time spent in sub nodes
  134. *
  135. * @return the time spent for the fastest invocation, in milliseconds
  136. */
  137. public double getMinTimeSpent() {
  138. return minTime;
  139. }
  140. /**
  141. * Gets the maximum time spent for one invocation of this node,
  142. * including time spent in sub nodes
  143. *
  144. * @return the time spent for the slowest invocation, in milliseconds
  145. */
  146. public double getMaxTimeSpent() {
  147. return maxTime;
  148. }
  149. /**
  150. * Gets the number of times this node has been entered
  151. *
  152. * @return the number of times the node has been entered
  153. */
  154. public int getCount() {
  155. return count;
  156. }
  157. /**
  158. * Gets the total time spent in this node, excluding time spent in sub
  159. * nodes
  160. *
  161. * @return the total time spent, in milliseconds
  162. */
  163. public double getOwnTime() {
  164. double time = getTimeSpent();
  165. for (Node node : children.values()) {
  166. time -= node.getTimeSpent();
  167. }
  168. return time;
  169. }
  170. /**
  171. * Gets the child nodes of this node
  172. *
  173. * @return a collection of child nodes
  174. */
  175. public Collection<Node> getChildren() {
  176. return Collections.unmodifiableCollection(children.values());
  177. }
  178. private void buildRecursiveString(StringBuilder builder,
  179. String prefix) {
  180. if (getName() != null) {
  181. String msg = getStringRepresentation(prefix);
  182. builder.append(msg + '\n');
  183. }
  184. String childPrefix = prefix + "*";
  185. for (Node node : children.values()) {
  186. node.buildRecursiveString(builder, childPrefix);
  187. }
  188. }
  189. @Override
  190. public String toString() {
  191. return getStringRepresentation("");
  192. }
  193. public String getStringRepresentation(String prefix) {
  194. if (getName() == null) {
  195. return "";
  196. }
  197. String msg = prefix + " " + getName() + " in "
  198. + roundToSignificantFigures(getTimeSpent()) + " ms.";
  199. if (getCount() > 1) {
  200. msg += " Invoked " + getCount() + " times ("
  201. + roundToSignificantFigures(getTimeSpent() / getCount())
  202. + " ms per time, min "
  203. + roundToSignificantFigures(getMinTimeSpent())
  204. + " ms, max "
  205. + roundToSignificantFigures(getMaxTimeSpent())
  206. + " ms).";
  207. }
  208. if (!children.isEmpty()) {
  209. double ownTime = getOwnTime();
  210. msg += " " + roundToSignificantFigures(ownTime)
  211. + " ms spent in own code";
  212. if (getCount() > 1) {
  213. msg += " ("
  214. + roundToSignificantFigures(ownTime / getCount())
  215. + " ms per time)";
  216. }
  217. msg += '.';
  218. }
  219. return msg;
  220. }
  221. private static double roundToSignificantFigures(double num) {
  222. // Number of significant digits
  223. int n = 3;
  224. if (num == 0) {
  225. return 0;
  226. }
  227. final double d = Math.ceil(Math.log10(num < 0 ? -num : num));
  228. final int power = n - (int) d;
  229. final double magnitude = Math.pow(10, power);
  230. final long shifted = Math.round(num * magnitude);
  231. return shifted / magnitude;
  232. }
  233. public void sumUpTotals(Map<String, Node> totals) {
  234. String name = getName();
  235. if (name != null) {
  236. Node totalNode = totals.get(name);
  237. if (totalNode == null) {
  238. totalNode = new Node(name);
  239. totals.put(name, totalNode);
  240. }
  241. totalNode.time += getOwnTime();
  242. totalNode.count += getCount();
  243. totalNode.minTime = roundToSignificantFigures(
  244. Math.min(totalNode.minTime, getMinTimeSpent()));
  245. totalNode.maxTime = roundToSignificantFigures(
  246. Math.max(totalNode.maxTime, getMaxTimeSpent()));
  247. }
  248. for (Node node : children.values()) {
  249. node.sumUpTotals(totals);
  250. }
  251. }
  252. /**
  253. * @param timestamp
  254. */
  255. public void leave(double timestamp) {
  256. double elapsed = (timestamp - enterTime);
  257. time += elapsed;
  258. enterTime = 0;
  259. if (elapsed < minTime) {
  260. minTime = elapsed;
  261. }
  262. if (elapsed > maxTime) {
  263. maxTime = elapsed;
  264. }
  265. }
  266. }
  267. private static final class GwtStatsEvent extends JavaScriptObject {
  268. protected GwtStatsEvent() {
  269. // JSO constructor
  270. }
  271. private native String getEvtGroup()
  272. /*-{
  273. return this.evtGroup;
  274. }-*/;
  275. private native double getMillis()
  276. /*-{
  277. return this.millis;
  278. }-*/;
  279. private native String getSubSystem()
  280. /*-{
  281. return this.subSystem;
  282. }-*/;
  283. private native String getType()
  284. /*-{
  285. return this.type;
  286. }-*/;
  287. private native String getModuleName()
  288. /*-{
  289. return this.moduleName;
  290. }-*/;
  291. private native double getRelativeMillis()
  292. /*-{
  293. return this.relativeMillis;
  294. }-*/;
  295. private native boolean isExtendedEvent()
  296. /*-{
  297. return 'relativeMillis' in this;
  298. }-*/;
  299. public final String getEventName() {
  300. String group = getEvtGroup();
  301. if (evtGroup.equals(group)) {
  302. return getSubSystem();
  303. } else {
  304. return group + "." + getSubSystem();
  305. }
  306. }
  307. }
  308. /**
  309. * Checks whether the profiling gathering is enabled.
  310. *
  311. * @return <code>true</code> if the profiling is enabled, else
  312. * <code>false</code>
  313. */
  314. public static boolean isEnabled() {
  315. // This will be fully inlined by the compiler
  316. Profiler create = GWT.create(Profiler.class);
  317. return create.isImplEnabled();
  318. }
  319. /**
  320. * Enters a named block. There should always be a matching invocation of
  321. * {@link #leave(String)} when leaving the block. Calls to this method will
  322. * be removed by the compiler unless profiling is enabled.
  323. *
  324. * @param name
  325. * the name of the entered block
  326. */
  327. public static void enter(String name) {
  328. if (isEnabled()) {
  329. logGwtEvent(name, "begin");
  330. }
  331. }
  332. /**
  333. * Leaves a named block. There should always be a matching invocation of
  334. * {@link #enter(String)} when entering the block. Calls to this method will
  335. * be removed by the compiler unless profiling is enabled.
  336. *
  337. * @param name
  338. * the name of the left block
  339. */
  340. public static void leave(String name) {
  341. if (isEnabled()) {
  342. logGwtEvent(name, "end");
  343. }
  344. }
  345. /**
  346. * Returns time relative to the particular page load time. The value should
  347. * not be used directly but rather difference between two values returned by
  348. * this method should be used to compare measurements.
  349. *
  350. * @since 7.6
  351. */
  352. public static double getRelativeTimeMillis() {
  353. return RELATIVE_TIME_SUPPLIER.getRelativeTime();
  354. }
  355. private static native final void logGwtEvent(String name, String type)
  356. /*-{
  357. $wnd.__gwtStatsEvent({
  358. evtGroup: @com.vaadin.client.Profiler::evtGroup,
  359. moduleName: @com.google.gwt.core.client.GWT::getModuleName()(),
  360. millis: (new Date).getTime(),
  361. sessionId: undefined,
  362. subSystem: name,
  363. type: type,
  364. relativeMillis: @com.vaadin.client.Profiler::getRelativeTimeMillis()()
  365. });
  366. }-*/;
  367. /**
  368. * Resets the collected profiler data. Calls to this method will be removed
  369. * by the compiler unless profiling is enabled.
  370. */
  371. public static void reset() {
  372. if (isEnabled()) {
  373. /*
  374. * Old implementations might call reset for initialization, so
  375. * ensure it is initialized here as well. Initialization has no side
  376. * effects if already done.
  377. */
  378. initialize();
  379. clearEventsList();
  380. }
  381. }
  382. /**
  383. * Initializes the profiler. This should be done before calling any other
  384. * function in this class. Failing to do so might cause undesired behavior.
  385. * This method has no side effects if the initialization has already been
  386. * done.
  387. * <p>
  388. * Please note that this method should be called even if the profiler is not
  389. * enabled because it will then remove a logger function that might have
  390. * been included in the HTML page and that would leak memory unless removed.
  391. * </p>
  392. *
  393. * @since 7.0.2
  394. */
  395. public static void initialize() {
  396. if (hasHighPrecisionTime()) {
  397. RELATIVE_TIME_SUPPLIER = new HighResolutionTimeSupplier();
  398. } else {
  399. RELATIVE_TIME_SUPPLIER = new DefaultRelativeTimeSupplier();
  400. }
  401. if (isEnabled()) {
  402. ensureLogger();
  403. } else {
  404. ensureNoLogger();
  405. }
  406. }
  407. /**
  408. * Outputs the gathered profiling data to the debug console.
  409. */
  410. public static void logTimings() {
  411. if (!isEnabled()) {
  412. getLogger().warning(
  413. "Profiler is not enabled, no data has been collected.");
  414. return;
  415. }
  416. LinkedList<Node> stack = new LinkedList<>();
  417. Node rootNode = new Node(null);
  418. stack.add(rootNode);
  419. JsArray<GwtStatsEvent> gwtStatsEvents = getGwtStatsEvents();
  420. if (gwtStatsEvents.length() == 0) {
  421. getLogger().warning(
  422. "No profiling events recorded, this might happen if another __gwtStatsEvent handler is installed.");
  423. return;
  424. }
  425. Set<Node> extendedTimeNodes = new HashSet<>();
  426. for (int i = 0; i < gwtStatsEvents.length(); i++) {
  427. GwtStatsEvent gwtStatsEvent = gwtStatsEvents.get(i);
  428. if (!evtGroup.equals(gwtStatsEvent.getEvtGroup())) {
  429. // Only log our own events to avoid problems with events which
  430. // are not of type start+end
  431. continue;
  432. }
  433. String eventName = gwtStatsEvent.getEventName();
  434. String type = gwtStatsEvent.getType();
  435. boolean isExtendedEvent = gwtStatsEvent.isExtendedEvent();
  436. boolean isBeginEvent = "begin".equals(type);
  437. Node stackTop = stack.getLast();
  438. boolean inEvent = eventName.equals(stackTop.getName())
  439. && !isBeginEvent;
  440. if (!inEvent && stack.size() >= 2
  441. && eventName.equals(stack.get(stack.size() - 2).getName())
  442. && !isBeginEvent) {
  443. // back out of sub event
  444. if (extendedTimeNodes.contains(stackTop) && isExtendedEvent) {
  445. stackTop.leave(gwtStatsEvent.getRelativeMillis());
  446. } else {
  447. stackTop.leave(gwtStatsEvent.getMillis());
  448. }
  449. stack.removeLast();
  450. stackTop = stack.getLast();
  451. inEvent = true;
  452. }
  453. if (type.equals("end")) {
  454. if (!inEvent) {
  455. getLogger().severe("Got end event for " + eventName
  456. + " but is currently in " + stackTop.getName());
  457. return;
  458. }
  459. Node previousStackTop = stack.removeLast();
  460. if (extendedTimeNodes.contains(previousStackTop)) {
  461. previousStackTop.leave(gwtStatsEvent.getRelativeMillis());
  462. } else {
  463. previousStackTop.leave(gwtStatsEvent.getMillis());
  464. }
  465. } else {
  466. double millis = isExtendedEvent
  467. ? gwtStatsEvent.getRelativeMillis()
  468. : gwtStatsEvent.getMillis();
  469. if (!inEvent) {
  470. stackTop = stackTop.enterChild(eventName, millis);
  471. stack.add(stackTop);
  472. if (isExtendedEvent) {
  473. extendedTimeNodes.add(stackTop);
  474. }
  475. }
  476. if (!isBeginEvent) {
  477. // Create sub event
  478. Node subNode = stackTop.enterChild(eventName + "." + type,
  479. millis);
  480. if (isExtendedEvent) {
  481. extendedTimeNodes.add(subNode);
  482. }
  483. stack.add(subNode);
  484. }
  485. }
  486. }
  487. if (stack.size() != 1) {
  488. getLogger().warning("Not all nodes are left, the last node is "
  489. + stack.getLast().getName());
  490. return;
  491. }
  492. Map<String, Node> totals = new HashMap<>();
  493. rootNode.sumUpTotals(totals);
  494. ArrayList<Node> totalList = new ArrayList<>(totals.values());
  495. Collections.sort(totalList, new Comparator<Node>() {
  496. @Override
  497. public int compare(Node o1, Node o2) {
  498. return (int) (o2.getTimeSpent() - o1.getTimeSpent());
  499. }
  500. });
  501. if (getConsumer() != null) {
  502. getConsumer().addProfilerData(stack.getFirst(), totalList);
  503. }
  504. }
  505. /**
  506. * Overridden in {@link EnabledProfiler} to make {@link #isEnabled()} return
  507. * true if GWT.create returns that class.
  508. *
  509. * @return <code>true</code> if the profiling is enabled, else
  510. * <code>false</code>
  511. */
  512. protected boolean isImplEnabled() {
  513. return false;
  514. }
  515. /**
  516. * Outputs the time passed since various events recored in
  517. * performance.timing if supported by the browser.
  518. */
  519. public static void logBootstrapTimings() {
  520. if (isEnabled()) {
  521. double now = Duration.currentTimeMillis();
  522. String[] keys = new String[] { "navigationStart",
  523. "unloadEventStart", "unloadEventEnd", "redirectStart",
  524. "redirectEnd", "fetchStart", "domainLookupStart",
  525. "domainLookupEnd", "connectStart", "connectEnd",
  526. "requestStart", "responseStart", "responseEnd",
  527. "domLoading", "domInteractive",
  528. "domContentLoadedEventStart", "domContentLoadedEventEnd",
  529. "domComplete", "loadEventStart", "loadEventEnd" };
  530. LinkedHashMap<String, Double> timings = new LinkedHashMap<>();
  531. for (String key : keys) {
  532. double value = getPerformanceTiming(key);
  533. if (value == 0) {
  534. // Ignore missing value
  535. continue;
  536. }
  537. timings.put(key, Double.valueOf(now - value));
  538. }
  539. if (timings.isEmpty()) {
  540. getLogger().info(
  541. "Bootstrap timings not supported, please ensure your browser supports performance.timing");
  542. return;
  543. }
  544. if (getConsumer() != null) {
  545. getConsumer().addBootstrapData(timings);
  546. }
  547. }
  548. }
  549. private static final native double getPerformanceTiming(String name)
  550. /*-{
  551. if ($wnd.performance && $wnd.performance.timing && $wnd.performance.timing[name]) {
  552. return $wnd.performance.timing[name];
  553. } else {
  554. return 0;
  555. }
  556. }-*/;
  557. private static native JsArray<GwtStatsEvent> getGwtStatsEvents()
  558. /*-{
  559. return $wnd.vaadin.gwtStatsEvents || [];
  560. }-*/;
  561. /**
  562. * Add logger if it's not already there, also initializing the event array
  563. * if needed.
  564. */
  565. private static native void ensureLogger()
  566. /*-{
  567. if (typeof $wnd.__gwtStatsEvent != 'function') {
  568. if (typeof $wnd.vaadin.gwtStatsEvents != 'object') {
  569. $wnd.vaadin.gwtStatsEvents = [];
  570. }
  571. $wnd.__gwtStatsEvent = function(event) {
  572. $wnd.vaadin.gwtStatsEvents.push(event);
  573. return true;
  574. }
  575. }
  576. }-*/;
  577. /**
  578. * Remove logger function and event array if it seems like the function has
  579. * been added by us.
  580. */
  581. private static native void ensureNoLogger()
  582. /*-{
  583. if (typeof $wnd.vaadin.gwtStatsEvents == 'object') {
  584. delete $wnd.vaadin.gwtStatsEvents;
  585. if (typeof $wnd.__gwtStatsEvent == 'function') {
  586. $wnd.__gwtStatsEvent = function() { return true; };
  587. }
  588. }
  589. }-*/;
  590. private static native JsArray<GwtStatsEvent> clearEventsList()
  591. /*-{
  592. $wnd.vaadin.gwtStatsEvents = [];
  593. }-*/;
  594. /**
  595. * Sets the profiler result consumer that is used to output the profiler
  596. * data to the user.
  597. * <p>
  598. * <b>Warning!</b> This is internal API and should not be used by
  599. * applications or add-ons.
  600. *
  601. * @since 7.1.4
  602. * @param profilerResultConsumer
  603. * the consumer that gets profiler data
  604. */
  605. public static void setProfilerResultConsumer(
  606. ProfilerResultConsumer profilerResultConsumer) {
  607. if (consumer != null) {
  608. throw new IllegalStateException(
  609. "The consumer has already been set");
  610. }
  611. consumer = profilerResultConsumer;
  612. }
  613. private static ProfilerResultConsumer getConsumer() {
  614. return consumer;
  615. }
  616. private static Logger getLogger() {
  617. return Logger.getLogger(Profiler.class.getName());
  618. }
  619. private static native boolean hasHighPrecisionTime()
  620. /*-{
  621. return $wnd.performance && (typeof $wnd.performance.now == 'function');
  622. }-*/;
  623. private interface RelativeTimeSupplier {
  624. double getRelativeTime();
  625. }
  626. private static class DefaultRelativeTimeSupplier
  627. implements RelativeTimeSupplier {
  628. @Override
  629. public native double getRelativeTime()
  630. /*-{
  631. return (new Date).getTime();
  632. }-*/;
  633. }
  634. private static class HighResolutionTimeSupplier
  635. implements RelativeTimeSupplier {
  636. @Override
  637. public native double getRelativeTime()
  638. /*-{
  639. return $wnd.performance.now();
  640. }-*/;
  641. }
  642. }