├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ └── Bug_report.md ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── _config.yml ├── cache └── .gitignore ├── composer.json ├── composer.lock ├── config └── config.default.php ├── external ├── header.php └── import.php ├── install.php ├── phpunit.xml ├── src ├── Xhgui │ ├── Config.php │ ├── Controller.php │ ├── Controller │ │ ├── Custom.php │ │ ├── Run.php │ │ ├── Watch.php │ │ └── Waterfall.php │ ├── Db │ │ └── Mapper.php │ ├── Middleware │ │ └── Render.php │ ├── Profile.php │ ├── Profiles.php │ ├── Saver.php │ ├── Saver │ │ ├── File.php │ │ ├── Interface.php │ │ └── Mongo.php │ ├── ServiceContainer.php │ ├── Twig │ │ └── Extension.php │ ├── Util.php │ └── WatchFunctions.php ├── bootstrap.php ├── routes.php └── templates │ ├── custom │ ├── create.twig │ ├── help.twig │ └── list.twig │ ├── error │ └── view.twig │ ├── layout │ └── base.twig │ ├── macros │ └── helpers.twig │ ├── runs │ ├── callgraph.twig │ ├── compare-details.twig │ ├── compare.twig │ ├── flamegraph.twig │ ├── list.twig │ ├── paginated-list.twig │ ├── symbol-short.twig │ ├── symbol.twig │ ├── url.twig │ └── view.twig │ ├── watch │ └── list.twig │ └── waterfall │ └── list.twig ├── tests ├── ConfigTest.php ├── Controller │ ├── RunTest.php │ └── WatchTest.php ├── Db │ └── MapperTest.php ├── ProfileTest.php ├── ProfilesTest.php ├── Saver │ ├── FileTest.php │ └── MongoTest.php ├── Twig │ └── ExtensionTest.php ├── WatchFunctionsTest.php ├── bootstrap.php └── fixtures │ └── results.json └── webroot ├── .htaccess ├── css ├── bootstrap.css ├── bootstrap.min.css ├── d3.flameGraph.css ├── datepicker.css └── xhgui.css ├── fonts ├── Chunkfive-webfont.eot ├── Chunkfive-webfont.svg ├── Chunkfive-webfont.ttf ├── Chunkfive-webfont.woff └── Open Font License.markdown ├── img ├── asc.gif ├── bg.gif ├── desc.gif ├── glyphicons-halflings-white.png ├── glyphicons-halflings.png ├── icon.png └── loading.gif ├── index.php ├── js ├── bootstrap-datepicker.js ├── bootstrap-tooltip.js ├── bootstrap.js ├── bootstrap.min.js ├── callgraph.js ├── d3-tip-index.js ├── d3.flameGraph.js ├── d3.js ├── dagre-d3.js ├── jquery.js ├── jquery.stickytableheaders.js ├── jquery.tablesorter.js ├── laydate.js ├── need │ └── laydate.css ├── skins │ ├── dahong │ │ ├── icon.png │ │ └── laydate.css │ ├── default │ │ ├── icon.png │ │ └── laydate.css │ └── molv │ │ ├── icon.png │ │ └── laydate.css ├── waterfall.js ├── xhgui-charts.js └── xhgui.js └── web.config /.gitattributes: -------------------------------------------------------------------------------- 1 | .js linguist-language=PHP 2 | .css linguist-language=PHP 3 | .html linguist-language=PHP 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: BUG提交模版 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **一定要读** 8 | 在提交BUG之前请查阅README文件,常见的安装与配置问题都有对应解决办法 。再查看历史ISSUE https://github.com/laynefyc/xhgui-branch/issues?utf8=%E2%9C%93&q=is%3Aissue 是否有答案。都解决不了,请认真填写如下信息 9 | 10 | **版本信息:** 11 | - 操作系统: [e.g. CentOS 6.8] 12 | - PHP [e.g. PHP7.1.13] 13 | - 扩展 [e.g. tideway.so v1.1.14] 14 | - MongodDB 版本 [e.g. MongodDB v4.0.4] 15 | 16 | **PHP信息:** 17 | 1. 运行php -m 将结果粘贴到当前文本中 18 | 2. 重启php-fpm 问题是否还在? 19 | 3. 直接在浏览器中访问 http://test.com/xhgui-branch/external/header.php (修改为自己的URL)是否报错 20 | 21 | **具体报错信息:** 22 | 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.phar 2 | /tags 3 | /vendor 4 | config/config.php 5 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: true 3 | 4 | filter: 5 | paths: ['src/*', 'external/*'] 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | 8 | matrix: 9 | include: 10 | - php: 5.5 11 | env: COVERAGE=1 12 | 13 | services: 14 | - mongodb 15 | 16 | before_script: 17 | - pecl install xhprof-beta 18 | - pecl install mongodb || true 19 | - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 20 | - git clone https://github.com/tideways/php-profiler-extension.git 21 | - cd php-profiler-extension && phpize && ./configure && make && make install && cd .. 22 | - echo "extension=tideways.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 23 | - echo "tideways.auto_prepend_library=0" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 24 | - phpenv rehash 25 | - composer install --prefer-dist --dev 26 | - chmod -R 0777 cache/ 27 | 28 | script: 29 | - > 30 | if [[ "$COVERAGE" == "0" ]]; then 31 | echo; 32 | echo "Running unit tests"; 33 | phpunit 34 | fi 35 | - > 36 | if [[ "$COVERAGE" == "1" ]]; then 37 | echo; 38 | echo "Running unit tests with code-coverage"; 39 | phpunit --coverage-clover=unittest-coverage.clover 40 | fi 41 | - > 42 | if [[ "$COVERAGE" == "1" ]]; then 43 | echo; 44 | echo "Uploading code coverage results"; 45 | wget https://scrutinizer-ci.com/ocular.phar 46 | php ocular.phar code-coverage:upload --format=php-clover unittest-coverage.clover 47 | fi 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of Xhgui Branch is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in Xhgui Branch to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open Source Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people’s personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone’s consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Consequences of Unacceptable Behavior 47 | 48 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 49 | 50 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 51 | 52 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 53 | 54 | ## 6. Reporting Guidelines 55 | 56 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. . 57 | 58 | 59 | 60 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 61 | 62 | ## 7. Addressing Grievances 63 | 64 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify Laynefyc with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 65 | 66 | 67 | 68 | ## 8. Scope 69 | 70 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business. 71 | 72 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 73 | 74 | ## 9. Contact info 75 | 76 | 77 | 78 | ## 10. License and attribution 79 | 80 | This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 81 | 82 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 83 | 84 | Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/) 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 最新 2 | 新版本已开发完成,做了如下改进: 3 | 4 | 🌈 支持本地Sqlite存储,也支持MySQL和MongoDB存储。 5 | 6 | 🌍 国际化,支持英语/简体中文/繁体中文。 7 | 8 | 🛡 可通过Composer极速安装。 9 | 10 | 🎨 一句命令运行监控服务。 11 | 12 | 🌈 支持帐号密码登录。 13 | 14 | 更多新功能欢迎前往 https://github.com/laynefyc/php-monitor 体验。 15 | 16 | ## 历史 17 | 新版本已经在开发中,后端功能全部开发完成,前端页面开发还在努力中。由于前端技术更新太快,不打算jQuery一把梭了,新版本会用上vue或者react,所以我也是边学习边开发。 18 | 19 | 公众号写了有一段时间,里面的内容也充实了不少,相比博客质量更高一些,分享的内容更成体系,有兴趣的关注看看。 20 | 21 | [http://imgs.it2048.cn/code-log.png](http://imgs.it2048.cn/code-log.png) 22 | 23 | 提交Issue之前请看看 README.md(当前页面)和历史已经处理的 [Closed Issue](https://github.com/laynefyc/xhgui-branch/issues?q=is%3Aissue+is%3Aclosed)90%的问题都能找到答案。另外10%的问题请按照Issue模板中需要的信息提交,你提供的信息越多我越能给你准确的建议,不按照规范提交的Issue我会直接关闭。 24 | 25 | 2019年想定个目标 - 重写xhprof扩展和xhgui-branch,有兴趣的朋友请移步到我的博客留言交流一下 [我的博客](http://blog.it2048.cn/) 26 | 27 | 获取底层信息的PHP扩展很多,比如 uprofiler,tideways_xhprof,tideways,xhprof等,他们的原理都一样,只是兼容性与稳定性的差别(选择一个安装,安装多个会冲突)。 28 | 29 | ```` 30 | Class 'MongoClient' not found 31 | Fatal error: Call to undefined function xhprof_enable() 32 | ```` 33 | 如出现上面的报错信息,请使用`php -m` 看看是否有tideways或者tideways_xhprof扩展。 再修改 [config/config.default.php#L12](https://github.com/laynefyc/xhgui-branch/blob/ad6e0c0a3eaf9b5b0438cd4a3d3db937f1954058/config/config.default.php#L12) 配置文件的扩展名。 如果还有问题请检查vendor 目录下是否存在 alcaeus/mongo-php-adapter扩展文件(这是一个兼容mongo.so和mongodb.so的适配器)。如果不存在请更新代码(git pull origin master),然后运行composer install安装。 34 | 35 | 90%的问题都能在 **ISSUE** 中找到答案 [ISSUE](https://github.com/laynefyc/xhgui-branch/issues?q=is%3Aissue+is%3Aclosed) 36 | 37 | tideways的新版扩展已经更名,并且不支持SQL显示,建议使用支持SQL展示的V4版本 [v4.1.6](https://github.com/tideways/php-xhprof-extension/tree/v4.1.6) 38 | 39 | 如果一定要使用V5版本,请修改配置文件 [config/config.default.php#L12](https://github.com/laynefyc/xhgui-branch/blob/ad6e0c0a3eaf9b5b0438cd4a3d3db937f1954058/config/config.default.php#L12) 为 tideways_xhprof 40 | 41 | 已添加SQL列表与SQL执行时间展示(暂时只支持tideways扩展),下文有截图。 42 | 43 | ## xhgui汉化与更新 44 | 45 | xhgui的安装信息可到源项目查看文档:[xhgui](https://github.com/perftools/xhgui) 46 | 47 | 如果不能安装成功可到我博客看这篇文章:[Tideways和xhgui打造PHP非侵入式监控平台](http://blog.it2048.cn/article_tideways-xhgui.html) 48 | 49 | 当然最好的方式就是联系我,我的博客:[https://blog.it2048.cn](https://blog.it2048.cn) 50 | 51 | [![Latest Stable Version](https://poser.pugx.org/laynefyc/xhgui-chinese/v/stable.png)](https://packagist.org/packages/laynefyc/xhgui-chinese) 52 | [![Total Downloads](https://poser.pugx.org/laynefyc/xhgui-chinese/downloads.png)](https://packagist.org/packages/laynefyc/xhgui-chinese) 53 | [![Build Status](https://travis-ci.org/laynefyc/xhgui-branch.svg?branch=master)](https://travis-ci.org/laynefyc/xhgui-branch) 54 | 55 | ### 一. 站在举人的肩膀上 56 | 57 | 项目的汉化参考了 [https://github.com/snfnwgi/xhgui](https://github.com/snfnwgi/xhgui),对部分翻译不够准确的词做了修改,对未翻译的部分做了翻译。 58 | 59 | xhgui源项目已经很久不更新了。我在基于xhgui搭建PHP监控平台的过程中遇到很多问题,自己对PHP和前端都还算了解,打算边修边优化并将更新的代码开源。 60 | 61 | ### 二. 为什么不直接在源项目提交Merge Request? 62 | 63 | 我会将一些基本的语法Bug修复后提交Merge Request。但汉化的修改不会提,主要原因是xhgui源项目对代码的要求基本是可用就可的程度,后期扩展的添加混乱的一塌糊涂。维护代码的人也焦头烂额,很多显而易见的错误都没人修。我无法保证我提的代码被及时的采纳。xhgui的UI主要是针对老外设计的,很多符号和数据单位我看着不习惯,一些交互也不友好,这个项目主要会对这方面做改动所以不适合提交Merge Request。 64 | 65 | ### 三. 界面截图 66 | 首页截图 67 | ![首页截图](https://github.com/laynefyc/xhgui-branch/raw/screenshot/screenshot/homepage.png) 68 | 69 | 瀑布图 70 | ![瀑布图](https://github.com/laynefyc/xhgui-branch/raw/screenshot/screenshot/waterfall.png) 71 | 72 | 函数监控图 73 | ![函数监控图](https://github.com/laynefyc/xhgui-branch/raw/screenshot/screenshot/view-function.png) 74 | 75 | SQL列表 76 | ![SQL列表](https://github.com/laynefyc/xhgui-branch/raw/screenshot/screenshot/sql_list.png) 77 | 78 | ### 四. 更新日志 79 | 1. 将时间选择控件换成了更符合国人使用习惯的laydate; 80 | 2. 将时间的格式转换成了 2017-06-08 12:18:18 格式; 81 | 3. 将微妙转换成了毫秒,byte转换成了MB或者KB; 82 | 4. 添加了IP的展示; 83 | 5. 将中文URL做了url_decode(); 84 | 6. 将页面的大标题去掉,换成用颜色选中的Nav标签展示; 85 | 7. 修复了『自定义函数』功能无法使用的问题; 86 | 8. 翻译了大量英文描述; 87 | 9. 很多小Tips等待有心人去发现; 88 | 10. 支持composer更新; 89 | 90 | ### 五. TODO 91 | 1. 将前端展示页面抽离出来; 92 | 2. 支持多域名的显示; 93 | 94 | ### 六. 通过Composer安装&更新 95 | 96 | ````bash 97 | composer require laynefyc/xhgui-chinese 98 | ```` 99 | 100 | ### 七. 常见问题 101 | 1. 如果数据显示不全,内存和执行等信息都是空,请排查PHP扩展程序,tideways和xhprof并不支持所有操作系统和所有PHP版本; 102 | 2. 如果mongoDB中的数据是空,请检查mongoDB的配置,header.php文件的引入是否规范; 103 | 3. 提交Issues请带上操作系统,PHP版本,扩展名和扩展版本。只提供一句话很难给你建议; 104 | 4. 历史问题在这里 [https://github.com/laynefyc/xhgui-branch/issues?utf8=%E2%9C%93&q=is%3Aissue](https://github.com/laynefyc/xhgui-branch/issues?utf8=%E2%9C%93&q=is%3Aissue) 105 | 106 | ## 八. 既然看到这了不如加个微信吧 107 | 108 | ![https://github.com/laynefyc/xhgui-branch/blob/screenshot/screenshot/code-log1.png](https://github.com/laynefyc/xhgui-branch/blob/screenshot/screenshot/code-log1.png) 109 | 110 | [http://imgs.it2048.cn/code-log.png](http://imgs.it2048.cn/code-log.png) 111 | 112 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- 1 | *.php 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laynefyc/xhgui-chinese", 3 | "description": "From perftools/xhgui,A web based interface for viewing profile data collected by XHProf.", 4 | "license": "MIT", 5 | "autoload": { 6 | "psr-0": { 7 | "Xhgui_": "src/" 8 | } 9 | }, 10 | "require": { 11 | "php": ">=5.3.0", 12 | "slim/slim": "^2.6.3", 13 | "slim/views": "^0.1.0", 14 | "twig/twig": "~1.17", 15 | "pimple/pimple": "^1.0.2", 16 | "alcaeus/mongo-php-adapter": "^1.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "3.7.x-dev", 20 | "ext-dom": "*" 21 | }, 22 | "suggest": { 23 | "ext-xhprof": "You need to install either xhprof or uprofiler to use XHGui.", 24 | "ext-uprofiler": "You need to install either xhprof or uprofiler to use XHGui.", 25 | "ext-mongo": "Mongo is needed to store profiler results for PHP < 7.", 26 | "ext-mongodb": "Mongo is needed to store profiler results for PHP > 7.", 27 | "alcaeus/mongo-php-adapter": "Mongo PHP Adapter is required for PHP >7 (when using ext-mongodb)" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config/config.default.php: -------------------------------------------------------------------------------- 1 | false, 7 | 'mode' => 'development', 8 | /* 9 | * support extension: uprofiler, tideways_xhprof, tideways, xhprof 10 | * default: xhprof 11 | */ 12 | 'extension' => 'xhprof', 13 | 14 | // Can be either mongodb or file. 15 | /* 16 | 'save.handler' => 'file', 17 | 'save.handler.filename' => dirname(__DIR__) . '/cache/' . 'xhgui.data.' . microtime(true) . '_' . substr(md5($url), 0, 6), 18 | */ 19 | 'save.handler' => 'mongodb', 20 | 21 | // Needed for file save handler. Beware of file locking. You can adujst this file path 22 | // to reduce locking problems (eg uniqid, time ...) 23 | //'save.handler.filename' => __DIR__.'/../data/xhgui_'.date('Ymd').'.dat', 24 | 'db.host' => 'mongodb://127.0.0.1:27017', 25 | 'db.db' => 'xhprof', 26 | 27 | // Allows you to pass additional options like replicaSet to MongoClient. 28 | // 'username', 'password' and 'db' (where the user is added) 29 | 'db.options' => array(), 30 | 'templates.path' => dirname(__DIR__) . '/src/templates', 31 | 'date.format' => 'Y-m-d H:i:s', 32 | 'detail.count' => 6, 33 | 'page.limit' => 25, 34 | 35 | // Profile 1 in 100 requests. 36 | // You can return true to profile every request. 37 | 'profiler.enable' => function() { 38 | return true;//rand(1, 100) === 42; 39 | }, 40 | 41 | 'profiler.simple_url' => function($url) { 42 | return preg_replace('/\=\d+/', '', $url); 43 | }, 44 | 45 | 'profiler.filter_path' => array( 46 | //'/home/admin/www/xhgui/webroot','F:/phpPro' 47 | ) 48 | 49 | ); 50 | -------------------------------------------------------------------------------- /external/header.php: -------------------------------------------------------------------------------- 1 | 4) { 106 | xhprof_enable(XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS); 107 | } else { 108 | xhprof_enable(XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY); 109 | } 110 | }else{ 111 | throw new Exception("Please check the extension name in config/config.default.php \r\n,you can use the 'php -m' command.", 1); 112 | } 113 | 114 | register_shutdown_function( 115 | function () { 116 | $extension = Xhgui_Config::read('extension'); 117 | if ($extension == 'uprofiler' && extension_loaded('uprofiler')) { 118 | $data['profile'] = uprofiler_disable(); 119 | } else if ($extension == 'tideways_xhprof' && extension_loaded('tideways_xhprof')) { 120 | $data['profile'] = tideways_xhprof_disable(); 121 | } else if ($extension == 'tideways' && extension_loaded('tideways')) { 122 | $data['profile'] = tideways_disable(); 123 | $sqlData = tideways_get_spans(); 124 | $data['sql'] = array(); 125 | if(isset($sqlData[1])){ 126 | foreach($sqlData as $val){ 127 | if(isset($val['n'])&&$val['n'] === 'sql'&&isset($val['a'])&&isset($val['a']['sql'])){ 128 | $_time_tmp = (isset($val['b'][0])&&isset($val['e'][0]))?($val['e'][0]-$val['b'][0]):0; 129 | if(!empty($val['a']['sql'])){ 130 | $data['sql'][] = array( 131 | 'time' => $_time_tmp, 132 | 'sql' => $val['a']['sql'] 133 | ); 134 | } 135 | } 136 | } 137 | } 138 | } else { 139 | $data['profile'] = xhprof_disable(); 140 | } 141 | 142 | // ignore_user_abort(true) allows your PHP script to continue executing, even if the user has terminated their request. 143 | // Further Reading: http://blog.preinheimer.com/index.php?/archives/248-When-does-a-user-abort.html 144 | // flush() asks PHP to send any data remaining in the output buffers. This is normally done when the script completes, but 145 | // since we're delaying that a bit by dealing with the xhprof stuff, we'll do it now to avoid making the user wait. 146 | ignore_user_abort(true); 147 | flush(); 148 | 149 | if (!defined('XHGUI_ROOT_DIR')) { 150 | require dirname(dirname(__FILE__)) . '/src/bootstrap.php'; 151 | } 152 | 153 | $uri = array_key_exists('REQUEST_URI', $_SERVER) 154 | ? $_SERVER['REQUEST_URI'] 155 | : null; 156 | if (empty($uri) && isset($_SERVER['argv'])) { 157 | $cmd = basename($_SERVER['argv'][0]); 158 | $uri = $cmd . ' ' . implode(' ', array_slice($_SERVER['argv'], 1)); 159 | } 160 | 161 | $time = array_key_exists('REQUEST_TIME', $_SERVER) 162 | ? $_SERVER['REQUEST_TIME'] 163 | : time(); 164 | $requestTimeFloat = explode('.', $_SERVER['REQUEST_TIME_FLOAT']); 165 | if (!isset($requestTimeFloat[1])) { 166 | $requestTimeFloat[1] = 0; 167 | } 168 | 169 | if (Xhgui_Config::read('save.handler') === 'file') { 170 | $requestTs = array('sec' => $time, 'usec' => 0); 171 | $requestTsMicro = array('sec' => $requestTimeFloat[0], 'usec' => $requestTimeFloat[1]); 172 | } else { 173 | $requestTs = new MongoDate($time); 174 | $requestTsMicro = new MongoDate($requestTimeFloat[0], $requestTimeFloat[1]); 175 | } 176 | 177 | $data['meta'] = array( 178 | 'url' => $uri, 179 | 'SERVER' => $_SERVER, 180 | 'get' => $_GET, 181 | 'env' => $_ENV, 182 | 'simple_url' => Xhgui_Util::simpleUrl($uri), 183 | 'request_ts' => $requestTs, 184 | 'request_ts_micro' => $requestTsMicro, 185 | 'request_date' => date('Y-m-d', $time), 186 | ); 187 | 188 | try { 189 | $config = Xhgui_Config::all(); 190 | $config += array('db.options' => array()); 191 | $saver = Xhgui_Saver::factory($config); 192 | $saver->save($data); 193 | } catch (Exception $e) { 194 | error_log('xhgui - ' . $e->getMessage()); 195 | } 196 | } 197 | ); 198 | -------------------------------------------------------------------------------- /external/import.php: -------------------------------------------------------------------------------- 1 | save($data); 33 | } 34 | } 35 | fclose($fp); 36 | -------------------------------------------------------------------------------- /install.php: -------------------------------------------------------------------------------- 1 | array('pipe', 'r'), 24 | 1 => array('pipe', 'w'), 25 | 2 => array('pipe', 'w') 26 | ); 27 | $process = proc_open( 28 | $cmd, 29 | $descriptorSpec, 30 | $pipes 31 | ); 32 | if (!is_resource($process)) { 33 | return 'ERROR - Could not start subprocess.'; 34 | } 35 | $output = $error = ''; 36 | fwrite($pipes[0], $input); 37 | fclose($pipes[0]); 38 | 39 | $output = stream_get_contents($pipes[1]); 40 | fclose($pipes[1]); 41 | 42 | $error = stream_get_contents($pipes[2]); 43 | fclose($pipes[2]); 44 | proc_close($process); 45 | if (strlen($error)) { 46 | return 'ERROR - ' . $error; 47 | } 48 | return $output; 49 | } 50 | 51 | /** 52 | * Composer setup. 53 | */ 54 | if (!file_exists(__DIR__ . '/composer.phar')) { 55 | out("Downloading composer."); 56 | $cmd = "php -r \"eval('?>'.file_get_contents('https://getcomposer.org/installer'));\""; 57 | $output = runProcess($cmd); 58 | out($output); 59 | } else { 60 | out("Composer already installed."); 61 | } 62 | 63 | if (!file_exists(__DIR__ . '/composer.phar')) { 64 | out('ERROR - No composer found'); 65 | out('download failed, possible reasons:'); 66 | out(' - you\'re behind a proxy.'); 67 | out(' - composer servers is not available at the moment.'); 68 | out(' - something wrong with network configuration.'); 69 | out('please try download it manually from https://getcomposer.org/installer and follow manual.'); 70 | out(''); 71 | exit(9); 72 | } 73 | 74 | out("Installing dependencies."); 75 | $cmd = 'php ' . __DIR__ . '/composer.phar install --prefer-dist'; 76 | $output = runProcess($cmd); 77 | out($output); 78 | 79 | 80 | /** 81 | * File permissions. 82 | */ 83 | out('Checking permissions for cache directory.'); 84 | $worldWritable = bindec('0000000111'); 85 | 86 | // Get current permissions in decimal format so we can bitmask it. 87 | $currentPerms = octdec(substr(sprintf('%o', fileperms('./cache')), -4)); 88 | 89 | if (($currentPerms & $worldWritable) != $worldWritable) { 90 | out('Attempting to set permissions on cache/'); 91 | $result = chmod(__DIR__ . '/cache', $currentPerms | $worldWritable); 92 | if ($result) { 93 | out('Permissions set on cache/'); 94 | } else { 95 | out('Failed to set permissions on cache/ you must do it yourself.'); 96 | } 97 | } else { 98 | out('Permissions on cache/ are ok.'); 99 | } 100 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./web/vendor 7 | 8 | 9 | 10 | 11 | 12 | ./tests 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Xhgui/Config.php: -------------------------------------------------------------------------------- 1 | _templateVars = array_merge($this->_templateVars, $vars); 11 | } 12 | 13 | public function templateVars() 14 | { 15 | return $this->_templateVars; 16 | } 17 | 18 | public function render() 19 | { 20 | $this->_app->render($this->_template, $this->_templateVars); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Xhgui/Controller/Custom.php: -------------------------------------------------------------------------------- 1 | _app = $app; 11 | $this->_profiles = $profiles; 12 | } 13 | 14 | public function get() 15 | { 16 | $this->_template = 'custom/create.twig'; 17 | $this->set(array( 18 | 'title' => '自定义查询' 19 | )); 20 | } 21 | 22 | public function help() 23 | { 24 | $request = $this->_app->request(); 25 | if ($request->get('id')) { 26 | $res = $this->_profiles->get($request->get('id')); 27 | } else { 28 | $res = $this->_profiles->latest(); 29 | } 30 | $this->_template = 'custom/help.twig'; 31 | $this->set(array( 32 | 'data' => print_r($res->toArray(), 1), 33 | 'title' => '自定义查询' 34 | )); 35 | } 36 | 37 | public function query() 38 | { 39 | $request = $this->_app->request(); 40 | $response = $this->_app->response(); 41 | $response['Content-Type'] = 'application/json'; 42 | 43 | $query = json_decode($request->post('query'),true); 44 | $error = array(); 45 | if (is_null($query)) { 46 | $error['query'] = json_last_error(); 47 | } 48 | 49 | $retrieve = json_decode($request->post('retrieve'),true); 50 | if (is_null($retrieve)) { 51 | $error['retrieve'] = json_last_error(); 52 | } 53 | 54 | if (count($error) > 0) { 55 | $json = json_encode(array('error' => $error)); 56 | return $response->body($json); 57 | } 58 | 59 | $perPage = $this->_app->config('page.limit'); 60 | 61 | $res = $this->_profiles->query($query, $retrieve) 62 | ->limit($perPage); 63 | $r = iterator_to_array($res); 64 | return $response->body(json_encode($r)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Xhgui/Controller/Watch.php: -------------------------------------------------------------------------------- 1 | _app = $app; 12 | $this->_watches = $watches; 13 | } 14 | 15 | public function get() 16 | { 17 | $watched = $this->_watches->getAll(); 18 | 19 | $this->_template = 'watch/list.twig'; 20 | $this->set(array('watched' => $watched,'title' => '函数监控')); 21 | } 22 | 23 | public function post() 24 | { 25 | $app = $this->_app; 26 | $watches = $this->_watches; 27 | 28 | $saved = false; 29 | $request = $app->request(); 30 | foreach ((array)$request->post('watch') as $data) { 31 | $saved = true; 32 | $watches->save($data); 33 | } 34 | if ($saved) { 35 | $app->flash('success', 'Watch functions updated.'); 36 | } 37 | $app->redirect($app->urlFor('watch.list')); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Xhgui/Controller/Waterfall.php: -------------------------------------------------------------------------------- 1 | _app = $app; 11 | $this->_profiles = $profiles; 12 | } 13 | 14 | public function index() 15 | { 16 | $request = $this->_app->request(); 17 | $search = array(); 18 | $keys = array("remote_addr", 'request_start', 'request_end'); 19 | foreach ($keys as $key) { 20 | if ($request->get($key)) { 21 | $search[$key] = trim($request->get($key)); 22 | } 23 | } 24 | 25 | if(isset($search['request_start']) && strpos($search['request_start'],'-')!==false) 26 | { 27 | $search['request_start'] = strtotime($search['request_start']); 28 | } 29 | 30 | if(isset($search['request_end']) && strpos($search['request_end'],'-')!==false) 31 | { 32 | $search['request_end'] = strtotime($search['request_end']); 33 | } 34 | 35 | $result = $this->_profiles->getAll(array( 36 | 'sort' => 'time', 37 | 'direction' => 'asc', 38 | 'conditions' => $search, 39 | 'projection' => true 40 | )); 41 | 42 | $paging = array( 43 | 'total_pages' => $result['totalPages'], 44 | 'page' => $result['page'], 45 | 'sort' => 'asc', 46 | 'direction' => $result['direction'] 47 | ); 48 | 49 | $this->_template = 'waterfall/list.twig'; 50 | 51 | 52 | if(isset($search['request_start'])&& strpos($search['request_start'],'-')===false) 53 | { 54 | $search['request_start'] = date('Y-m-d H:i:s',$search['request_start']); 55 | } 56 | if(isset($search['request_end'])&& strpos($search['request_end'],'-')===false) 57 | { 58 | $search['request_end'] = date('Y-m-d H:i:s',$search['request_end']); 59 | } 60 | $this->set(array( 61 | 'runs' => $result['results'], 62 | 'search' => $search, 63 | 'paging' => $paging, 64 | 'base_url' => 'waterfall.list', 65 | 'title' => '瀑布图', 66 | 'date_format' => $this->_app->config('date.format') 67 | )); 68 | } 69 | 70 | public function query() 71 | { 72 | $request = $this->_app->request(); 73 | $response = $this->_app->response(); 74 | $search = array(); 75 | $keys = array("remote_addr", 'request_start', 'request_end'); 76 | foreach ($keys as $key) { 77 | $search[$key] = $request->get($key); 78 | } 79 | $result = $this->_profiles->getAll(array( 80 | 'sort' => 'time', 81 | 'direction' => 'asc', 82 | 'conditions' => $search, 83 | 'projection' => TRUE 84 | )); 85 | $datas = array(); 86 | foreach ($result['results'] as $r) { 87 | $duration = $r->get('main()', 'wt'); 88 | $start = $r->getMeta('SERVER.REQUEST_TIME_FLOAT'); 89 | $title = $r->getMeta('url'); 90 | $datas[] = array( 91 | 'id' => (string)$r->getId(), 92 | 'title' => $title, 93 | 'start' => $start * 1000, 94 | 'duration' => $duration / 1000 // Convert to correct scale 95 | ); 96 | } 97 | $response->body(json_encode($datas)); 98 | $response['Content-Type'] = 'application/json'; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/Xhgui/Db/Mapper.php: -------------------------------------------------------------------------------- 1 | array(), 16 | 'sort' => null, 17 | 'direction' => null, 18 | 'perPage' => 25 19 | ); 20 | if (isset($options['conditions'])) { 21 | $result['conditions'] = $this->_conditions($options['conditions']); 22 | } 23 | $result['direction'] = $this->_direction($options); 24 | $result['sort'] = $this->_sort($options); 25 | 26 | if (isset($options['perPage'])) { 27 | $result['perPage'] = $options['perPage']; 28 | } 29 | 30 | return $result; 31 | } 32 | 33 | /** 34 | * Convert the search parameters into the matching fields. 35 | * 36 | * Keeps the schema details out of the GET parameters. 37 | * String casts are uses to prevent mongo operator injection. 38 | * 39 | * @param array $search 40 | * @return array 41 | */ 42 | protected function _conditions($search) 43 | { 44 | if (!empty($search['limit_custom']) && $search['limit_custom'][0] == "P") { 45 | $search['limit'] = $search['limit_custom']; 46 | } 47 | $hasLimit = (!empty($search['limit']) && $search['limit'] != -1); 48 | 49 | $conditions = array(); 50 | if (!empty($search['date_start']) && !$hasLimit) { 51 | $conditions['meta.request_date']['$gte'] = (string)$search['date_start']; 52 | } 53 | if (!empty($search['date_end']) && !$hasLimit) { 54 | $conditions['meta.request_date']['$lte'] = (string)$search['date_end']; 55 | } 56 | if (isset($search['simple_url'])) { 57 | $conditions['meta.simple_url'] = (string)$search['simple_url']; 58 | } 59 | if (!empty($search['request_start'])) { 60 | $conditions['meta.SERVER.REQUEST_TIME']['$gte'] = $this->_convertDate($search['request_start']); 61 | } 62 | if (!empty($search['request_end'])) { 63 | $conditions['meta.SERVER.REQUEST_TIME']['$lte'] = $this->_convertDate($search['request_end']); 64 | } 65 | 66 | if (!empty($search['remote_addr'])) { 67 | $conditions['meta.SERVER.REMOTE_ADDR'] = (string)$search['remote_addr']; 68 | } 69 | if (isset($search['cookie'])) { 70 | $conditions['meta.SERVER.HTTP_COOKIE'] = (string)$search['cookie']; 71 | } 72 | 73 | if ($hasLimit && $search['limit'][0] == "P") { 74 | $date = new DateTime(); 75 | try { 76 | $date->sub(new DateInterval($search['limit'])); 77 | $conditions['meta.request_ts']['$gte'] = new MongoDate($date->getTimestamp()); 78 | } catch (\Exception $e) { 79 | // Match a day in the future so we match nothing, as it's likely an invalid format 80 | $conditions['meta.request_ts']['$gte'] = new MongoDate(time() + 86400); 81 | } 82 | } 83 | 84 | if (isset($search['url'])) { 85 | // Not sure if letting people use regex here 86 | // is a good idea. Only one way to find out. 87 | $conditions['meta.url'] = array( 88 | '$regex' => (string)$search['url'], 89 | '$options' => 'i', 90 | ); 91 | } 92 | 93 | return $conditions; 94 | } 95 | 96 | protected function _convertDate($dateString) 97 | { 98 | if (is_numeric($dateString)) { 99 | return (float) $dateString; 100 | } 101 | $date = DateTime::createFromFormat('Y-m-d H:i:s', $dateString); 102 | if (!$date) { 103 | return $date; 104 | } 105 | return $date->getTimestamp(); 106 | } 107 | 108 | protected function _direction($options) 109 | { 110 | if (empty($options['direction'])) { 111 | return 'desc'; 112 | } 113 | $valid = array('desc', 'asc'); 114 | if (in_array($options['direction'], $valid, true)) { 115 | return $options['direction']; 116 | } 117 | return 'desc'; 118 | } 119 | /** 120 | * Get sort options for a paginated set. 121 | * 122 | * Whitelists to valid known keys. 123 | * 124 | * @param array $options Pagination options including the sort key. 125 | * @return array Sort field & direction. 126 | */ 127 | protected function _sort($options) 128 | { 129 | $direction = -1; 130 | if (isset($options['direction']) && $options['direction'] === 'asc') { 131 | $direction = 1; 132 | } 133 | 134 | $valid = array('time', 'wt', 'mu', 'cpu'); 135 | if ( 136 | empty($options['sort']) || 137 | (isset($options['sort']) && !in_array($options['sort'], $valid)) 138 | ) { 139 | return array('meta.SERVER.REQUEST_TIME' => $direction); 140 | } 141 | if ($options['sort'] == 'time') { 142 | return array('meta.SERVER.REQUEST_TIME' => $direction); 143 | } elseif ($options['sort'] == 'wt') { 144 | return array('profile.main().wt' => $direction); 145 | } elseif ($options['sort'] == 'mu') { 146 | return array('profile.main().mu' => $direction); 147 | } elseif ($options['sort'] == 'cpu') { 148 | return array('profile.main().cpu' => $direction); 149 | } 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/Xhgui/Middleware/Render.php: -------------------------------------------------------------------------------- 1 | app; 9 | 10 | // Run the controller action/route function 11 | $this->next->call(); 12 | 13 | // Render the template. 14 | if (isset($app->controller)) { 15 | $app->controller->render(); 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/Xhgui/Saver.php: -------------------------------------------------------------------------------- 1 | {$config['db.db']}->results->findOne(); 25 | $profiles = new Xhgui_Profiles($mongo->{$config['db.db']}); 26 | return new Xhgui_Saver_Mongo($profiles); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Xhgui/Saver/File.php: -------------------------------------------------------------------------------- 1 | _file = $file; 10 | } 11 | 12 | public function save($data) 13 | { 14 | $json = json_encode($data); 15 | return file_put_contents($this->_file, $json.PHP_EOL, FILE_APPEND); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Xhgui/Saver/Interface.php: -------------------------------------------------------------------------------- 1 | _profiles = $profiles; 10 | } 11 | 12 | public function save($data) 13 | { 14 | return $this->_profiles->insert($data); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Xhgui/ServiceContainer.php: -------------------------------------------------------------------------------- 1 | _slimApp(); 22 | $this->_services(); 23 | $this->_controllers(); 24 | } 25 | 26 | // Create the Slim app. 27 | protected function _slimApp() 28 | { 29 | $this['view'] = function ($c) { 30 | $cacheDir = isset($c['config']['cache']) ? $c['config']['cache'] : XHGUI_ROOT_DIR . '/cache'; 31 | 32 | // Configure Twig view for slim 33 | $view = new Twig(); 34 | 35 | $view->parserOptions = array( 36 | 'charset' => 'utf-8', 37 | 'cache' => $cacheDir, 38 | 'auto_reload' => true, 39 | 'strict_variables' => false, 40 | 'autoescape' => true 41 | ); 42 | 43 | return $view; 44 | }; 45 | 46 | $this['app'] = $this->share(function ($c) { 47 | $app = new Slim($c['config']); 48 | 49 | // Enable cookie based sessions 50 | $app->add(new SessionCookie(array( 51 | 'httponly' => true, 52 | ))); 53 | 54 | // Add renderer. 55 | $app->add(new Xhgui_Middleware_Render()); 56 | 57 | $view = $c['view']; 58 | $view->parserExtensions = array( 59 | new Xhgui_Twig_Extension($app) 60 | ); 61 | $app->view($view); 62 | 63 | return $app; 64 | }); 65 | } 66 | 67 | /** 68 | * Add common service objects to the container. 69 | */ 70 | protected function _services() 71 | { 72 | $this['db'] = $this->share(function ($c) { 73 | $config = $c['config']; 74 | if (empty($config['db.options'])) { 75 | $config['db.options'] = array(); 76 | } 77 | $mongo = new MongoClient($config['db.host'], $config['db.options']); 78 | $mongo->{$config['db.db']}->results->findOne(); 79 | 80 | return $mongo->{$config['db.db']}; 81 | }); 82 | 83 | $this['watchFunctions'] = function ($c) { 84 | return new Xhgui_WatchFunctions($c['db']); 85 | }; 86 | 87 | $this['profiles'] = function ($c) { 88 | return new Xhgui_Profiles($c['db']); 89 | }; 90 | 91 | $this['saver'] = function($c) { 92 | $config = $c['config']; 93 | switch ($config['save.handler']) { 94 | case 'file': 95 | return new Xhgui_Saver_File($config['save.handler.filename']); 96 | case 'mongodb': 97 | default: 98 | return $c['saverMongo']; 99 | } 100 | }; 101 | 102 | $this['saverMongo'] = function($c) { 103 | return new Xhgui_Saver_Mongo($c['profiles']); 104 | }; 105 | } 106 | 107 | /** 108 | * Add controllers to the DI container. 109 | */ 110 | protected function _controllers() 111 | { 112 | $this['watchController'] = function ($c) { 113 | return new Xhgui_Controller_Watch($c['app'], $c['watchFunctions']); 114 | }; 115 | 116 | $this['runController'] = function ($c) { 117 | return new Xhgui_Controller_Run($c['app'], $c['profiles'], $c['watchFunctions']); 118 | }; 119 | 120 | $this['customController'] = function ($c) { 121 | return new Xhgui_Controller_Custom($c['app'], $c['profiles']); 122 | }; 123 | 124 | $this['waterfallController'] = function ($c) { 125 | return new Xhgui_Controller_Waterfall($c['app'], $c['profiles']); 126 | }; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/Xhgui/Twig/Extension.php: -------------------------------------------------------------------------------- 1 | _app = $app; 10 | } 11 | 12 | public function getName() 13 | { 14 | return 'xhgui'; 15 | } 16 | 17 | public function getFunctions() 18 | { 19 | return array( 20 | 'url' => new Twig_Function_Method($this, 'url'), 21 | 'static' => new Twig_Function_Method($this, 'staticUrl'), 22 | 'percent' => new Twig_Function_Method($this, 'makePercent', array( 23 | 'is_safe' => array('html') 24 | )), 25 | ); 26 | } 27 | 28 | public function getFilters() 29 | { 30 | return array( 31 | 'simple_url' => new Twig_Filter_Function('Xhgui_Util::simpleUrl'), 32 | 'as_bytes' => new Twig_Filter_Method($this, 'formatBytes', array('is_safe' => array('html'))), 33 | 'as_kbytes' => new Twig_Filter_Method($this, 'formatKbytes', array('is_safe' => array('html'))), 34 | 'as_time' => new Twig_Filter_Method($this, 'formatTime', array('is_safe' => array('html'))), 35 | 'as_diff' => new Twig_Filter_Method($this, 'formatDiff', array('is_safe' => array('html'))), 36 | 'as_percent' => new Twig_Filter_Method($this, 'formatPercent', array('is_safe' => array('html'))), 37 | 'truncate' => new Twig_Filter_Method($this, 'truncate'), 38 | ); 39 | } 40 | 41 | protected function _getBase() 42 | { 43 | $base = dirname($_SERVER['PHP_SELF']); 44 | if ($base == '/') { 45 | return ''; 46 | } 47 | return $base; 48 | } 49 | 50 | public function truncate($input, $length = 50) 51 | { 52 | $input = urldecode($input); 53 | if (strlen($input) < $length) { 54 | return $input; 55 | } 56 | return substr($input, 0, $length) . "\xe2\x80\xa6"; 57 | } 58 | 59 | /** 60 | * Get a URL for xhgui. 61 | * 62 | * @param string $path The file/path you want a link to 63 | * @param array $queryarg Additional querystring arguments. 64 | * @return string url. 65 | */ 66 | public function url($name, $queryargs = array()) 67 | { 68 | $query = ''; 69 | if (!empty($queryargs)) { 70 | $query = '?' . http_build_query($queryargs); 71 | } 72 | return $this->_app->urlFor($name) . $query; 73 | } 74 | 75 | /** 76 | * Get the URL for static content relative to webroot 77 | * 78 | * @param string $path The file/path you want a link to 79 | * @return string url. 80 | */ 81 | public function staticUrl($url) 82 | { 83 | $rootUri = $this->_app->request()->getRootUri(); 84 | 85 | // Get URL part prepending index.php 86 | $indexPos = strpos($rootUri, 'index.php'); 87 | if ($indexPos > 0) { 88 | return substr($rootUri, 0, $indexPos) . $url; 89 | } 90 | return $rootUri . '/' . $url; 91 | } 92 | 93 | public function formatBytes($value) 94 | { 95 | $val = round($value/1048576,1); 96 | if($val==0) 97 | { 98 | $val = round($value/1048576); 99 | } 100 | return $val . ' MB'; 101 | } 102 | 103 | public function formatKbytes($value) 104 | { 105 | $val = round($value/1024,1); 106 | if($val==0) 107 | { 108 | $val = round($value/1024); 109 | } 110 | return $val . ' KB'; 111 | } 112 | 113 | public function formatTime($value) 114 | { 115 | $val = round($value/1000,1); 116 | if($val==0) 117 | { 118 | $val = round($value/1000); 119 | } 120 | return $val . ' ms'; 121 | } 122 | 123 | public function formatDiff($value) 124 | { 125 | $class = 'diff-same'; 126 | $class = $value > 0 ? 'diff-up' : 'diff-down'; 127 | if ($value == 0) { 128 | $class = 'diff-same'; 129 | } 130 | return sprintf( 131 | '%s', 132 | $class, 133 | number_format((float)$value) 134 | ); 135 | } 136 | 137 | public function makePercent($value, $total) 138 | { 139 | $value = (false === empty($total)) ? $value / $total : 0; 140 | return $this->formatPercent($value); 141 | } 142 | 143 | public function formatPercent($value) 144 | { 145 | return number_format((float)$value * 100, 0) . ' %'; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Xhgui/Util.php: -------------------------------------------------------------------------------- 1 | _collection = $db->watches; 9 | } 10 | 11 | /** 12 | * Save a value to the collection. 13 | * 14 | * Will do an insert or update depending 15 | * on the id field being present. 16 | * 17 | * @param array $data The data to save. 18 | * @return boolean 19 | */ 20 | public function save($data) 21 | { 22 | if (empty($data['name'])) { 23 | return false; 24 | } 25 | 26 | if (!empty($data['removed']) && isset($data['_id'])) { 27 | $this->_collection->remove( 28 | array('_id' => new MongoId($data['_id'])), 29 | array('w' => 1) 30 | ); 31 | return true; 32 | } 33 | 34 | if (empty($data['_id'])) { 35 | $this->_collection->insert( 36 | $data, 37 | array('w' => 1) 38 | ); 39 | return true; 40 | } 41 | 42 | 43 | $data['_id'] = new MongoId($data['_id']); 44 | $this->_collection->update( 45 | array('_id' => $data['_id']), 46 | $data, 47 | array('w' => 1) 48 | ); 49 | return true; 50 | } 51 | 52 | /** 53 | * Get all the known watch functions. 54 | * 55 | * @return array Array of watch functions. 56 | */ 57 | public function getAll() 58 | { 59 | $cursor = $this->_collection->find(); 60 | return array_values(iterator_to_array($cursor)); 61 | } 62 | 63 | /** 64 | * Truncate the watch collection. 65 | * 66 | * @return void 67 | */ 68 | public function truncate() 69 | { 70 | $this->_collection->drop(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/bootstrap.php: -------------------------------------------------------------------------------- 1 | error(function (Exception $e) use ($di, $app) { 6 | $view = $di['view']; 7 | $view->parserOptions['cache'] = false; 8 | $view->parserExtensions = array( 9 | new Xhgui_Twig_Extension($app) 10 | ); 11 | 12 | // Remove the controller so we don't render it. 13 | unset($app->controller); 14 | 15 | $app->view($view); 16 | $app->render('error/view.twig', array( 17 | 'message' => $e->getMessage(), 18 | 'stack_trace' => $e->getTraceAsString(), 19 | )); 20 | }); 21 | 22 | // Profile Runs routes 23 | $app->get('/', function () use ($di, $app) { 24 | $app->controller = $di['runController']; 25 | $app->controller->index(); 26 | })->name('home'); 27 | 28 | $app->get('/run/view', function () use ($di, $app) { 29 | $app->controller = $di['runController']; 30 | $app->controller->view(); 31 | })->name('run.view'); 32 | 33 | $app->get('/url/view', function () use ($di, $app) { 34 | $app->controller = $di['runController']; 35 | $app->controller->url(); 36 | })->name('url.view'); 37 | 38 | $app->get('/run/compare', function () use ($di, $app) { 39 | $app->controller = $di['runController']; 40 | $app->controller->compare(); 41 | })->name('run.compare'); 42 | 43 | $app->get('/run/symbol', function () use ($di, $app) { 44 | $app->controller = $di['runController']; 45 | $app->controller->symbol(); 46 | })->name('run.symbol'); 47 | 48 | $app->get('/run/symbol/short', function () use ($di, $app) { 49 | $app->controller = $di['runController']; 50 | $app->controller->symbolShort(); 51 | })->name('run.symbol-short'); 52 | 53 | $app->get('/run/callgraph', function () use ($di, $app) { 54 | $app->controller = $di['runController']; 55 | $app->controller->callgraph(); 56 | })->name('run.callgraph'); 57 | 58 | $app->get('/run/callgraph/data', function () use ($di, $app) { 59 | $di['runController']->callgraphData(); 60 | })->name('run.callgraph.data'); 61 | 62 | $app->get('/run/flamegraph', function () use ($di, $app) { 63 | $app->controller = $di['runController']; 64 | $app->controller->flamegraph(); 65 | })->name('run.flamegraph'); 66 | 67 | $app->get('/run/flamegraph/data', function () use ($di, $app) { 68 | $di['runController']->flamegraphData(); 69 | })->name('run.flamegraph.data'); 70 | 71 | $app->get('/run/callgraph/dot', function () use ($di, $app) { 72 | $di['runController']->callgraphDataDot(); 73 | })->name('run.callgraph.dot'); 74 | 75 | // Watch function routes. 76 | $app->get('/watch', function () use ($di, $app) { 77 | $app->controller = $di['watchController']; 78 | $app->controller->get(); 79 | })->name('watch.list'); 80 | 81 | $app->post('/watch', function () use ($di) { 82 | $di['watchController']->post(); 83 | })->name('watch.save'); 84 | 85 | 86 | // Custom report routes. 87 | $app->get('/custom', function () use ($di, $app) { 88 | $app->controller = $di['customController']; 89 | $app->controller->get(); 90 | })->name('custom.view'); 91 | 92 | $app->get('/custom/help', function () use ($di, $app) { 93 | $app->controller = $di['customController']; 94 | $app->controller->help(); 95 | })->name('custom.help'); 96 | 97 | $app->post('/custom/query', function () use ($di) { 98 | $di['customController']->query(); 99 | })->name('custom.query'); 100 | 101 | 102 | // Waterfall routes 103 | $app->get('/waterfall', function () use ($di, $app) { 104 | $app->controller = $di['waterfallController']; 105 | $app->controller->index(); 106 | })->name('waterfall.list'); 107 | 108 | $app->get('/waterfall/data', function () use ($di) { 109 | $di['waterfallController']->query(); 110 | })->name('waterfall.data'); 111 | -------------------------------------------------------------------------------- /src/templates/custom/create.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | 3 | {% block content %} 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 | {% for key, value in profile %} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {% endfor %} 66 | 67 |
请求地址时间执行时间CPU时间内存占用内存峰值
{{ key }}{{ value.ct | number_format }}{{ value.ewt | number_format }} µs{{ value.ecpu |number_format }} µs{{ value.emu |number_format }} bytes{{ value.epmu |number_format }} bytes{{ value.wt | number_format }} µs{{ value.cpu | number_format }} µs{{ value.mu | number_format }} bytes{{ value.pmu | number_format }} bytes
68 |
69 |
70 | {% endblock %} 71 | 72 | 73 | {% block jsfoot %} 74 | 125 | {% endblock %} 126 | -------------------------------------------------------------------------------- /src/templates/custom/help.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | 3 | {% block content %} 4 |
5 | 6 |
7 |
8 | 9 |
10 |

Selection Criteria

11 |

This will be passed as-is to MongoDB. You may use this to select (for example) hits against a specific URL, or requests that invoked MongoCollection::find() more than 3 times.

12 |

Samples

13 | Display hits against a given URL 14 |

15 | {"meta.url" : "/index.php"} 16 |

17 | Display runs that took more than 1 second 18 |

19 | { "profile.main().wt" : {"$gt" : 1000000}} 20 |

21 | 22 |
23 | 24 | 25 | 26 |
27 |

Values to Retrieve

28 |

It's unlikely you require the full dataset back, this allows you to grab just the values you care about to reduce transferred data.

29 |

Samples

30 | Retrieve meta details, and overall run data 31 |

32 | {"meta" : true, "profile.main()" : true} 33 |

34 |
35 | 36 |
37 |

Values to Graph

38 |

These are things that should end up on the graph

39 | 40 |
41 | 42 |
43 |
44 | 45 |
46 |

Raw Data

47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | {{ data }}
55 |         
56 |
57 | 58 |
59 | 60 |
61 |
62 | {% endblock %} 63 | 64 | -------------------------------------------------------------------------------- /src/templates/custom/list.twig: -------------------------------------------------------------------------------- 1 | {% for result in runs %} 2 | 3 | 4 | 5 | {{result.meta('SERVER.REQUEST_METHOD')}} 6 | 7 | 8 | 9 | {% set addr = result.meta.url %} 10 | {{ helpers.tip_link( 11 | addr, 12 | 50, 13 | 'url.view', 14 | {'url': result.meta.simple_url} 15 | ) }} 16 | 17 | {{ result.date|date(date_format) }} 18 | {{ result.get('main()', 'wt') |as_time }} 19 | {{ result.get('main()', 'cpu') |as_time }} 20 | {{ result.get('main()', 'mu') |as_bytes }} 21 | {{ result.get('main()', 'pmu') |as_bytes }} 22 | {{ result.meta.SERVER.REMOTE_ADDR }} 23 | 24 | 25 | {% endfor %} -------------------------------------------------------------------------------- /src/templates/error/view.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | 3 | {% block content %} 4 |

Aw shoot, Xhgui hit an error

5 | 6 |

{{ message }}

7 | 8 |

9 | You should check the following things: 10 |

11 | 16 | 17 |
Stack trace
18 |
{{ stack_trace }}
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/templates/layout/base.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }}-PHP性能分析平台 8 | 9 | 10 | {% block head '' %} 11 | 12 | 13 | 36 | 37 |
38 | {% if flash.success %} 39 |
40 | {{ flash.success }} 41 |
42 | {% endif %} 43 | 44 | {% block content '' %} 45 | 46 |
47 | 48 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {% block jsfoot '' %} 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/templates/macros/helpers.twig: -------------------------------------------------------------------------------- 1 | {% macro property_list(name, data) %} 2 |
3 | {% for k, v in data %} 4 |
{{ k }}
5 |
6 | {% if not v is iterable %} 7 | {{ v|join(' ') }} 8 | {% else %} 9 | {{ v|json_encode() }} 10 | {% endif %} 11 |
12 | {% else %} 13 |
No {{ name }} data
14 | {% endfor %} 15 |
16 | {% endmacro %} 17 | 18 | {% macro sort_link(text, url, sort, paging, search) %} 19 | {% set search = search|default({}) %} 20 | {% set classname = '' %} 21 | {% set direction = 'asc' %} 22 | {%- if paging.direction == 'asc' -%} 23 | {% set direction = 'desc' %} 24 | {%- endif -%} 25 | {% if paging.sort == sort %} 26 | {% set classname = "sort-#{direction}" %} 27 | {% endif %} 28 | {% set params = {'direction': direction, 'sort': sort}|merge(search) %} 29 | 30 | {{ text }} 31 | {% endmacro %} 32 | 33 | {% macro tip_link(text, len, url, params) %} 34 | len %}class="tip"{% endif %} 35 | href="{{ url(url, params) }}" 36 | title="{{ text }}"> 37 | {{ text|truncate(len) }} 38 | 39 | {% endmacro %} 40 | 41 | {% macro pagination(url, paging, search) %} 42 | 43 | {# Hard code because I'm lazy #} 44 | {% set num_pages = 10 %} 45 | {% set midpoint = none %} 46 | {% set start = 1 %} 47 | {% set end = paging.total_pages|default(start) %} 48 | {% set total_pages = paging.total_pages|default(start) %} 49 | {% set search = search|default({}) %} 50 | {% set search = search|merge({direction: paging.direction, sort: paging.sort}) %} 51 | 52 | {# show a slice of pages in the middle #} 53 | {% if total_pages > num_pages %} 54 | {% set midpoint = (num_pages // 2) %} 55 | {% set start = paging.page - midpoint %} 56 | {% if start < 1 %} 57 | {% set start = 1 %} 58 | {% endif %} 59 | {% set end = paging.page + midpoint %} 60 | {% if end < num_pages %} 61 | {% set end = num_pages %} 62 | {% endif %} 63 | {% if end > total_pages %} 64 | {% set end = total_pages %} 65 | {% endif %} 66 | {% endif %} 67 | 68 | 69 | 92 | {% endmacro %} 93 | 94 | {% macro time_and_percent(prop, value, total) %} 95 | {{ value[prop]|as_time }}
96 | ({{ percent(value[prop], total[prop]) }}) 97 | {% endmacro %} 98 | 99 | {% macro bytes_and_percent(prop, value, total) %} 100 | {{ value[prop]|as_bytes }}
101 | ({{ percent(value[prop], total[prop]) }}) 102 | {% endmacro %} 103 | -------------------------------------------------------------------------------- /src/templates/runs/callgraph.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | 3 | {% block title %} 4 | - 调用图 - {{ profile.meta.url }} - {{ profile.meta('SERVER.REQUEST_TIME')|date(date_format) }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | « 返回 10 |

调用图: {{ profile.meta.url }}

11 |

{{ profile.meta('SERVER.REQUEST_TIME')|date(date_format) }}

12 |
13 | 14 |
15 |
16 | 17 | 更新 18 | 19 |
20 |
21 |
22 |

标准

23 |
24 | 34 |
35 |
36 | 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 |

由于受限于xhprof如何收集数据,调用图不能100%反映实际方法调用图。

68 |
69 | {% endblock %} 70 | 71 | {% block jsfoot %} 72 | 73 | 74 | 137 | {% endblock %} 138 | -------------------------------------------------------------------------------- /src/templates/runs/compare-details.twig: -------------------------------------------------------------------------------- 1 | {# Template fragment for comparing two runs. #} 2 |

摘要

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
 调用函数数量包含执行时间包含CPU时间包含内存占用包含内存占用峰值
17 | 18 | 基础 - {{ comparison.base.date|date(date_format) }} 19 | 20 | 21 | {{ comparison.base.id }} 22 | 23 | {{ comparison.base.functionCount }}{{ comparison.base.get('main()', 'wt') |as_time }}{{ comparison.base.get('main()', 'cpu') |as_time }}{{ comparison.base.get('main()', 'mu') |as_bytes }}{{ comparison.base.get('main()', 'pmu') |as_bytes }}
32 | 33 | 新 - {{ comparison.head.date|date(date_format) }} 34 | 35 | 36 | {{ comparison.head.id }} 37 | 38 | {{ comparison.head.functionCount }}{{ comparison.head.get('main()', 'wt') |as_time }}{{ comparison.head.get('main()', 'cpu') |as_time }}{{ comparison.head.get('main()', 'mu') |as_bytes }}{{ comparison.head.get('main()', 'pmu') |as_bytes }}
差异{{ comparison.diff.functionCount }}{{ comparison.diff['main()'].wt|as_time }}{{ comparison.diff['main()'].cpu|as_time }}{{ comparison.diff['main()'].mu|as_bytes }}{{ comparison.diff['main()'].pmu|as_bytes }}
差异 %{{ comparison.diffPercent.functionCount|as_percent }}{{ comparison.diffPercent['main()'].wt|as_percent }}{{ comparison.diffPercent['main()'].cpu|as_percent }}{{ comparison.diffPercent['main()'].mu|as_percent }}{{ comparison.diffPercent['main()'].pmu|as_percent }}
63 | 64 | 65 | 68 | 69 |

详细

70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {% for key, value in comparison.diff %} 88 | 89 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | {% endfor %} 103 | 104 |
函数调用次数自身执行时间自身CPU时间自身内存占用自身内存占用峰值包含执行时间包含CPU时间包含内存占用包含内存占用峰值
90 | {{ key }} 91 | {{ value.ct|as_diff }}{{ value.ewt|as_diff }}{{ value.ecpu|as_diff }}{{ value.emu|as_diff }}{{ value.epmu|as_diff }}{{ value.wt|as_diff }}{{ value.cpu|as_diff }}{{ value.mu|as_diff }}{{ value.pmu|as_diff }}
105 |

Red values are higher in 'new'. Green values are lower in 'new'.

106 | -------------------------------------------------------------------------------- /src/templates/runs/compare.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | {% import 'macros/helpers.twig' as helpers %} 3 | 4 | {% block title %} 5 | - 对比 - {{ base_run.meta.simple_url }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

对比运行:{{ base_run.meta.simple_url }}

10 | 11 | {% if not base_run %} 12 |
13 |

14 | 你必须选择一个基础运行才能开始比较。 15 |

16 |
17 | {% else %} 18 |
19 | 20 | 基础: {{ base_run.meta.simple_url }} - {{ base_run.date|date(date_format) }} 21 | 22 | 23 | 24 | {% if head_run %} 25 | 26 | 新: {{ _run.meta.simple_url }} - {{ head_run.date|date(date_format) }} 27 | 28 | 修改 29 | 交换 30 | {% else %} 31 | 在下面选择一个运行 32 | {% endif %} 33 |
34 | 35 | {% if candidates|length %} 36 |
37 |

和{{ base_run.meta.simple_url }}相关的其他运行

38 | {% include 'runs/paginated-list.twig' with {runs: candidates.results, show_compare_link: true} %} 39 | {{ helpers.pagination('run.compare', pagination, url_params) }} 40 |
41 | {% endif %} 42 | 43 | {% if comparison %} 44 |
45 | {% include 'runs/compare-details.twig' %} 46 |
47 | {% endif %} 48 | 49 | {% endif %} 50 | 51 | {% endblock %} 52 | 53 | 54 | {% block jsfoot %} 55 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /src/templates/runs/flamegraph.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | 3 | {% block head %} 4 | 5 | {% endblock %} 6 | 7 | {% block title %} 8 | - 火焰图 - {{ profile.meta.url }} - {{ profile.meta('SERVER.REQUEST_TIME')|date(date_format) }} 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 | « 返回 14 |

火焰图: {{ profile.meta.url }}

15 |

{{ profile.meta('SERVER.REQUEST_TIME')|date(date_format) }}

16 |
17 | 18 |
19 |
20 |
21 | 重置放大 22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 | 火焰图将长时间运行的方法以及有深层调用堆栈的方法可视化。 37 |
38 | {% endblock %} 39 | 40 | 41 | {% block jsfoot %} 42 | 43 | 44 | 45 | 119 | {% endblock %} 120 | -------------------------------------------------------------------------------- /src/templates/runs/list.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | {% import 'macros/helpers.twig' as helpers %} 3 | 4 | {% block title %} 5 | - 运行列表 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 | {% if runs|length or has_search %} 11 | 37 | {% endif %} 38 | 39 | {# No run data, and no search, must be just getting started #} 40 | {% if not runs|length and not has_search %} 41 |
42 |

看起来你没有做任何配置

43 |

你需要收集一些分析数据才能开始使用XHGUI

44 |

最简单的分析一个应用程序的方式是使用external/header.php。 45 | 这个文件设计用来在PHP的 46 | auto_prepend_file 47 | 指令中使用。你可以在系统的php.ini中启用. 或者你也可以在每个虚拟主机中启用 48 | auto_prepend_file。 更多详细的细节可以查看README.md文件。 49 |

50 |
51 | {% endif %} 52 | 53 | {% if runs|length or has_search %} 54 | {% include 'runs/paginated-list.twig' %} 55 | {% endif %} 56 | 57 | {% if runs|length %} 58 | {{ helpers.pagination('home', paging, search) }} 59 | {% endif %} 60 | 61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /src/templates/runs/paginated-list.twig: -------------------------------------------------------------------------------- 1 | {# Template fragment for outputting a list of runs #} 2 | {% import 'macros/helpers.twig' as helpers %} 3 |
4 | 5 | 6 | 7 | {% if show_compare_link %} 8 | 9 | {% endif %} 10 | 13 | 14 | 17 | 22 | 27 | 32 | 37 | 42 | 43 | 44 | 45 | {% for result in runs %} 46 | 47 | {% if show_compare_link %} 48 | 53 | {% endif %} 54 | 59 | 68 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | {% else %} 81 | 82 | 85 | 86 | {% endfor %} 87 | 88 |
  11 | 方法 12 | 请求地址 15 | {{ helpers.sort_link('时间', base_url, 'time', paging, search) }} 16 | 18 | 19 | {{ helpers.sort_link('执行时间', base_url, 'wt', paging, search) }} 20 | 21 | 23 | 24 | {{ helpers.sort_link('CPU时间', base_url, 'cpu', paging, search) }} 25 | 26 | 28 | 29 | {{ helpers.sort_link('内存占用', base_url, 'mu', paging, search) }} 30 | 31 | 33 | 34 | {{ helpers.sort_link('内存峰值', base_url, 'pmu', paging, search) }} 35 | 36 | 38 | 39 | IP地址 40 | 41 |
49 | 50 | 对比 51 | 52 | 55 | 56 | {{result.meta('SERVER.REQUEST_METHOD')}} 57 | 58 | 60 | {% set addr = result.meta.url %} 61 | {{ helpers.tip_link( 62 | addr, 63 | 50, 64 | 'url.view', 65 | {'url': result.meta.simple_url} 66 | ) }} 67 | 69 | 70 | {{ result.date|date(date_format) }} 71 | 72 | {{ result.get('main()', 'wt') |as_time }}{{ result.get('main()', 'cpu') |as_time }}{{ result.get('main()', 'mu') |as_bytes }}{{ result.get('main()', 'pmu') |as_bytes }}{{ result.meta.SERVER.REMOTE_ADDR }} 78 |
83 | 你的搜索条件没有匹配的结果,改变搜索条件试试。 84 |
89 |
90 | -------------------------------------------------------------------------------- /src/templates/runs/symbol-short.twig: -------------------------------------------------------------------------------- 1 | {% import 'macros/helpers.twig' as helpers %} 2 | 3 |
4 |

函数详细

5 |

{{ symbol }}

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 |
调用次数{{ current.ct }}
自身执行时间{{ current.ewt|as_time }}
自身CPU时间{{ current.ecpu|as_time }}
自身内存占用{{ current.emu|as_bytes }}
自身内存占用峰值{{ current.epmu|as_bytes }}
包含执行时间{{ helpers.time_and_percent('wt', current, main) }}
包含CPU时间{{ helpers.time_and_percent('cpu', current, main) }}
包含内存占用{{ helpers.time_and_percent('mu', current, main) }}
包含内存占用峰值{{ helpers.time_and_percent('pmu', current, main) }}
46 | 47 |

Parent functions

48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% for parent in parents %} 60 | 61 | 69 | 70 | 71 | 72 | 73 | 74 | {% else %} 75 | 76 | 77 | 78 | {% endfor %} 79 | 80 |
函数调用次数自身执行时间自身内存占用
62 | {{ helpers.tip_link( 63 | parent.function, 64 | 65, 65 | 'run.symbol', 66 | {'id': id, 'symbol': parent.function} 67 | ) }} 68 | {{ child.ct }}{{ helpers.time_and_percent('wt', parent, main) }}{{ parent.mu|as_bytes }}
{{ symbol }} 没有父函数。
81 | 82 |

Child functions

83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | {% for child in children %} 95 | 96 | 104 | 105 | 106 | 107 | 108 | 109 | {% else %} 110 | 111 | 112 | 113 | {% endfor %} 114 | 115 |
函数调用次数自身执行时间自身内存占用
97 | {{ helpers.tip_link( 98 | child.function, 99 | 65, 100 | 'run.symbol', 101 | {'id': id, 'symbol': child.function} 102 | ) }} 103 | {{ child.ct }}{{ helpers.time_and_percent('wt', child, main) }}{{ child.mu|as_bytes }}
{{ symbol }} 没有调用函数。
116 | 117 |
118 | -------------------------------------------------------------------------------- /src/templates/runs/symbol.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | {% import 'macros/helpers.twig' as helpers %} 3 | 4 | {% block title %} 5 | - 函数详细 - {{ symbol }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | « 返回 11 |

函数详细

12 |

{{ symbol }}

13 |
14 |
15 |

当前函数

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
函数调用次数自身执行时间自身CPU时间自身内存占用自身内存占用峰值包含执行时间包含CPU时间包含内存占用包含内存占用峰值
34 | {{ helpers.tip_link( 35 | current.function, 36 | 65, 37 | 'run.symbol', 38 | {'id': id, 'symbol': current.function} 39 | ) }} 40 |
41 | Percent of total request 42 |
{{ current.ct }}{{ current.ewt|as_time }}{{ current.ecpu|as_time }}{{ current.emu|as_bytes }}{{ current.epmu|as_bytes }}{{ helpers.time_and_percent('wt', current, main) }}{{ helpers.time_and_percent('cpu', current, main) }}{{ helpers.bytes_and_percent('mu', current, main) }}{{ helpers.bytes_and_percent('pmu', current, main) }}
56 | 57 |

父函数

58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {% for parent in parents %} 75 | 76 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {% endfor %} 98 | 99 |
函数调用次数自身执行时间自身CPU时间自身内存占用自身内存占用峰值包含执行时间包含CPU时间包含内存占用包含内存占用峰值
77 | {{ helpers.tip_link( 78 | parent.function, 79 | 65, 80 | 'run.symbol', 81 | {'id': id, 'symbol': parent.function} 82 | ) }} 83 |
84 | Percent of total request 85 |
{{ parent.ct }}{{ parent.ewt|as_time }}{{ parent.ecpu|as_time }}{{ parent.emu|as_bytes }}{{ parent.epmu|as_bytes }}{{ helpers.time_and_percent('wt', parent, main) }}{{ helpers.time_and_percent('cpu', parent, main) }}{{ helpers.bytes_and_percent('mu', parent, main) }}{{ helpers.bytes_and_percent('pmu', parent, main) }}
100 | 101 |

子函数

102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | {% for child in children %} 116 | 117 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {% else %} 133 | 134 | 135 | 136 | {% endfor %} 137 | 138 |
函数调用次数包含执行时间包含CPU时间包含内存占用包含内存占用峰值
118 | {{ helpers.tip_link( 119 | child.function, 120 | 65, 121 | 'run.symbol', 122 | {'id': id, 'symbol': child.function} 123 | ) }} 124 | {{ child.ct }}{{ child.wt|as_time }}{{ child.cpu|as_time }}{{ child.mu|as_bytes }}{{ child.pmu|as_bytes }}
{{ symbol }} 没有调用函数。
139 | 140 |
141 | 142 | {% endblock %} 143 | -------------------------------------------------------------------------------- /src/templates/runs/url.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | {% import 'macros/helpers.twig' as helpers %} 3 | 4 | {% block title %} 5 | - Url - {{ url }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

Runs for {{ url }} (simplified)

10 | 11 |
12 |
13 |
14 |
15 | 16 | 56 | 57 | {% include 'runs/paginated-list.twig' %} 58 | 59 | {{ helpers.pagination('url.view', paging, search) }} 60 | 61 | {% endblock %} 62 | 63 | {% block jsfoot %} 64 | 102 | {% endblock %} 103 | -------------------------------------------------------------------------------- /src/templates/runs/view.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | {% import 'macros/helpers.twig' as helpers %} 3 | 4 | {% block title %} 5 | - Profile - {{ result.meta.url }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 | 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 | {% for value in watches %} 65 | 66 | 74 | 75 | 76 | 77 | 78 | 79 | {% else %} 80 | 84 | {% endfor %} 85 | 86 |
函数调用次数自身执行时间自身内存占用自身内存峰值
67 | {{ helpers.tip_link( 68 | value.function, 69 | 65, 70 | 'run.symbol', 71 | {'id': result.id|trim, 'symbol': value.function} 72 | ) }} 73 | {{ value.ct }}{{ value.ewt|as_time }}{{ value.emu|as_bytes }}{{ value.epmu|as_bytes }}
81 | 你没有设置观察函数 82 | 现在去添加一个观察函数. 83 |
87 | 88 |

SQL执行详情

89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {% for value in sql %} 98 | 99 | 100 | 101 | 102 | {% else %} 103 | 106 | {% endfor %} 107 | 108 |
执行时间执行SQL
{{ value.time|as_time }}{{ value.sql }}
104 | 没有SQL调用 105 |
109 |
110 | 111 |
112 |
113 |

执行耗时图

114 |
115 |
116 | {% for value in wall_time %} 117 |
{{ value.name }}
118 |
{{ value.value }} ms
119 | {% endfor %} 120 |
121 |
122 | 123 |
124 |

内存消耗图

125 |
126 |
127 | {% for value in memory %} 128 |
{{ value.name }}
129 |
{{ value.value }} KB
130 | {% endfor %} 131 |
132 |
133 |
134 |
135 |
136 | 137 |
138 | 141 |
142 | 143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | {% for key, value in profile %} 162 | 163 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | {% endfor %} 182 | 183 |
函数调用次数执行时间CPU时间内存占用内存峰值总执行时间总CPU时间总内存占用总内存峰值
164 | {{ helpers.tip_link( 165 | key, 166 | 65, 167 | 'run.symbol', 168 | {'id': result.id|trim, 'symbol': key} 169 | ) }} 170 | {{ value.ct }}{{ value.ewt|as_time }}{{ value.ecpu|as_time }}{{ value.emu|as_kbytes }}{{ value.epmu|as_kbytes }}{{ value.wt|as_time }}{{ value.cpu|as_time }}{{ value.mu|as_kbytes }}{{ value.pmu|as_kbytes }}
184 |
185 | {% endblock %} 186 | 187 | {% block jsfoot %} 188 | 218 | {% endblock %} 219 | -------------------------------------------------------------------------------- /src/templates/watch/list.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | {% import 'macros/helpers.twig' as helpers %} 3 | 4 | {% block title %} 5 | - 观察列表 - 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

观察函数

10 | 11 |

12 | 观察函数允许你设置函数名称或模式用于显示在每个运行结果页面的顶部 13 |

14 |

15 | 观察函数可以精确匹配或使用PCRE模式匹配多个函数的名字。 16 |

17 | 18 |
19 | {% if watched|length %} 20 | 34 |

35 | 添加一个 36 |

37 | 38 |
39 | 40 |
41 | {% else %} 42 |
43 |

你还没有观察函数

44 |

观察函数可以让你感兴趣的函数显示在运行结果中。

45 |

开始添加一个

46 | 47 | 48 |
49 | {% endif %} 50 | 51 |
52 | 53 | {% endblock %} 54 | 55 | {% block jsfoot %} 56 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /src/templates/waterfall/list.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/base.twig' %} 2 | {% import 'macros/helpers.twig' as helpers %} 3 | 4 | {% block title %} 5 | - Waterfall - 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 | 41 | 42 |
43 | {% if search is empty %} 44 |
45 |

没有数据

46 |

你需要设置一个搜索条件,IP或者Cookie

47 |
48 | {% else %} 49 |
50 | {% endif %} 51 |
52 | 53 |
54 | 55 | 56 | 57 | {% if show_compare_link %} 58 | 59 | {% endif %} 60 | 63 | 64 | 67 | 72 | 77 | 82 | 87 | 92 | 93 | 94 | 95 | {% for result in runs %} 96 | 97 | {% if show_compare_link %} 98 | 103 | {% endif %} 104 | 109 | 118 | 123 | 124 | 125 | 126 | 127 | 129 | 130 | {% else %} 131 | 132 | 135 | 136 | {% endfor %} 137 | 138 |
  61 | 方法 62 | 请求地址 65 | {{ helpers.sort_link('时间', base_url, 'time', paging, search) }} 66 | 68 | 69 | {{ helpers.sort_link('执行时间', base_url, 'wt', paging, search) }} 70 | 71 | 73 | 74 | {{ helpers.sort_link('CPU时间', base_url, 'cpu', paging, search) }} 75 | 76 | 78 | 79 | {{ helpers.sort_link('内存占用', base_url, 'mu', paging, search) }} 80 | 81 | 83 | 84 | {{ helpers.sort_link('内存峰值', base_url, 'pmu', paging, search) }} 85 | 86 | 88 | 89 | IP地址 90 | 91 |
99 | 100 | Compare 101 | 102 | 105 | 106 | {{result.meta('SERVER.REQUEST_METHOD')}} 107 | 108 | 110 | {% set addr = result.meta.url %} 111 | {{ helpers.tip_link( 112 | addr, 113 | 50, 114 | 'url.view', 115 | {'url': result.meta.simple_url} 116 | ) }} 117 | 119 | 120 | {{ result.date|date(date_format) }} 121 | 122 | {{ result.get('main()', 'wt') |as_time }}{{ result.get('main()', 'cpu') |as_time }}{{ result.get('main()', 'mu') |as_bytes }}{{ result.get('main()', 'pmu') |as_bytes }}{{ result.meta.SERVER.REMOTE_ADDR }} 128 |
133 | Your search conditions matched no runs. Try changing you search criteria. 134 |
139 |
140 | 141 | 142 | 143 | {% endblock %} 144 | 145 | {% block jsfoot %} 146 | 147 | 160 | {% endblock %} 161 | -------------------------------------------------------------------------------- /tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | assertNull($result); 15 | 16 | $result = Xhgui_Config::read('test'); 17 | $this->assertEquals('value', $result); 18 | 19 | $result = Xhgui_Config::read('not there'); 20 | $this->assertNull($result); 21 | } 22 | 23 | public function testReadWriteWithDots() 24 | { 25 | $result = Xhgui_Config::write('test.name', 'value'); 26 | $this->assertNull(Xhgui_Config::read('test')); 27 | $this->assertEquals('value', Xhgui_Config::read('test.name')); 28 | } 29 | 30 | public function testClear() 31 | { 32 | Xhgui_Config::write('test', 'value'); 33 | $this->assertNull(Xhgui_Config::clear()); 34 | $this->assertNull(Xhgui_Config::read('test')); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /tests/Controller/RunTest.php: -------------------------------------------------------------------------------- 1 | 'index.php', 11 | 'PATH_INFO' => '/' 12 | )); 13 | 14 | $di = Xhgui_ServiceContainer::instance(); 15 | $mock = $this->getMock( 16 | 'Slim\Slim', 17 | array('redirect', 'render', 'urlFor'), 18 | array($di['config']) 19 | ); 20 | 21 | $di['app'] = $di->share(function ($c) use ($mock) { 22 | return $mock; 23 | }); 24 | $this->runs = $di['runController']; 25 | $this->app = $di['app']; 26 | $this->profiles = $di['profiles']; 27 | $this->profiles->truncate(); 28 | } 29 | 30 | public function testIndexEmpty() 31 | { 32 | $this->runs->index(); 33 | $result = $this->runs->templateVars(); 34 | 35 | $this->assertEquals('Recent runs', $result['title']); 36 | $this->assertFalse($result['has_search'], 'No search being done.'); 37 | $expected = array( 38 | 'total_pages' => 1, 39 | 'page' => 1, 40 | 'sort' => null, 41 | 'direction' => 'desc', 42 | ); 43 | $this->assertEquals($expected, $result['paging']); 44 | } 45 | 46 | public function testIndexSortedWallTime() 47 | { 48 | Environment::mock(array( 49 | 'SCRIPT_NAME' => 'index.php', 50 | 'PATH_INFO' => '/', 51 | 'QUERY_STRING' => 'sort=wt', 52 | )); 53 | 54 | $this->runs->index(); 55 | $result = $this->runs->templateVars(); 56 | $this->assertEquals('Longest wall time', $result['title']); 57 | $this->assertEquals('wt', $result['paging']['sort']); 58 | } 59 | 60 | public function testIndexSortedCpu() 61 | { 62 | Environment::mock(array( 63 | 'SCRIPT_NAME' => 'index.php', 64 | 'PATH_INFO' => '/', 65 | 'QUERY_STRING' => 'sort=cpu&direction=desc', 66 | )); 67 | 68 | $this->runs->index(); 69 | $result = $this->runs->templateVars(); 70 | $this->assertEquals('Most CPU time', $result['title']); 71 | $this->assertEquals('cpu', $result['paging']['sort']); 72 | $this->assertEquals('desc', $result['paging']['direction']); 73 | } 74 | 75 | public function testIndexWithSearch() 76 | { 77 | Environment::mock(array( 78 | 'SCRIPT_NAME' => 'index.php', 79 | 'PATH_INFO' => '/', 80 | 'QUERY_STRING' => 'sort=mu&direction=asc&url=index.php', 81 | )); 82 | 83 | $this->runs->index(); 84 | $result = $this->runs->templateVars(); 85 | $this->assertEquals('Highest memory use', $result['title']); 86 | $this->assertEquals('mu', $result['paging']['sort']); 87 | $this->assertEquals('asc', $result['paging']['direction']); 88 | $this->assertEquals(array('url' => 'index.php'), $result['search']); 89 | $this->assertTrue($result['has_search']); 90 | } 91 | 92 | public function testUrl() 93 | { 94 | Environment::mock(array( 95 | 'SCRIPT_NAME' => 'index.php', 96 | 'PATH_INFO' => '/url/view', 97 | 'QUERY_STRING' => 'url=%2Ftasks', 98 | )); 99 | 100 | $this->runs->url(); 101 | 102 | $result = $this->runs->templateVars(); 103 | $this->assertEquals('url.view', $result['base_url']); 104 | $this->assertEquals('/tasks', $result['url']); 105 | $this->assertArrayHasKey('chart_data', $result); 106 | $this->assertArrayHasKey('runs', $result); 107 | } 108 | 109 | public function testUrlWithSearch() 110 | { 111 | $this->markTestIncomplete('Not done'); 112 | } 113 | 114 | public function testUrlWithSearchInterval() 115 | { 116 | $this->markTestIncomplete('Not done'); 117 | } 118 | 119 | public function testCompareNoBase() 120 | { 121 | $this->markTestIncomplete('Not done'); 122 | } 123 | 124 | public function testCompareWithBase() 125 | { 126 | $this->markTestIncomplete('Not done'); 127 | } 128 | 129 | public function testCompareWithBaseAndHead() 130 | { 131 | $this->markTestIncomplete('Not done'); 132 | } 133 | 134 | public function testSymbol() 135 | { 136 | $this->markTestIncomplete('Not done'); 137 | } 138 | 139 | public function testCallgraph() 140 | { 141 | loadFixture($this->profiles, 'tests/fixtures/results.json'); 142 | Environment::mock(array( 143 | 'SCRIPT_NAME' => 'index.php', 144 | 'PATH_INFO' => '/', 145 | 'QUERY_STRING' => 'id=aaaaaaaaaaaaaaaaaaaaaaaa', 146 | )); 147 | 148 | $this->runs->callgraph(); 149 | $result = $this->runs->templateVars(); 150 | $this->assertArrayHasKey('profile', $result); 151 | $this->assertArrayHasKey('date_format', $result); 152 | $this->assertArrayNotHasKey('callgraph', $result); 153 | } 154 | 155 | public function testCallgraphData() 156 | { 157 | loadFixture($this->profiles, 'tests/fixtures/results.json'); 158 | Environment::mock(array( 159 | 'SCRIPT_NAME' => 'index.php', 160 | 'PATH_INFO' => '/', 161 | 'QUERY_STRING' => 'id=aaaaaaaaaaaaaaaaaaaaaaaa', 162 | )); 163 | 164 | $this->runs->callgraphData(); 165 | $response = $this->app->response(); 166 | 167 | $this->assertEquals('application/json', $response['Content-Type']); 168 | $this->assertStringStartsWith('{"', $response->body()); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /tests/Controller/WatchTest.php: -------------------------------------------------------------------------------- 1 | 'index.php', 11 | 'PATH_INFO' => '/watch' 12 | )); 13 | $di = Xhgui_ServiceContainer::instance(); 14 | unset($di['app']); 15 | 16 | $mock = $this->getMock( 17 | 'Slim\Slim', 18 | array('redirect', 'render', 'urlFor'), 19 | array($di['config']) 20 | ); 21 | $di['app'] = $di->share(function ($c) use ($mock) { 22 | return $mock; 23 | }); 24 | $this->watches = $di['watchController']; 25 | $this->app = $di['app']; 26 | $this->watchFunctions = $di['watchFunctions']; 27 | $this->watchFunctions->truncate(); 28 | } 29 | 30 | public function testGet() 31 | { 32 | $this->watches->get(); 33 | $result = $this->watches->templateVars(); 34 | $this->assertEquals(array(), $result['watched']); 35 | } 36 | 37 | public function testPostAdd() 38 | { 39 | $_POST = array( 40 | 'watch' => array( 41 | array('name' => 'strlen'), 42 | array('name' => 'strpos') 43 | ) 44 | ); 45 | $this->app->expects($this->once()) 46 | ->method('urlFor') 47 | ->with('watch.list'); 48 | 49 | $this->app->expects($this->once()) 50 | ->method('redirect'); 51 | 52 | $this->watches->post(); 53 | $result = $this->watchFunctions->getAll(); 54 | 55 | $this->assertCount(2, $result); 56 | $this->assertEquals('strlen', $result[0]['name']); 57 | $this->assertEquals('strpos', $result[1]['name']); 58 | } 59 | 60 | public function testPostModify() 61 | { 62 | $this->watchFunctions->save(array('name' => 'strlen')); 63 | $saved = $this->watchFunctions->getAll(); 64 | 65 | $_POST = array( 66 | 'watch' => array( 67 | array('name' => 'strpos', '_id' => $saved[0]['_id']) 68 | ) 69 | ); 70 | $this->watches->post(); 71 | $result = $this->watchFunctions->getAll(); 72 | 73 | $this->assertCount(1, $result); 74 | $this->assertEquals('strpos', $result[0]['name']); 75 | } 76 | 77 | public function testPostDelete() 78 | { 79 | $this->watchFunctions->save(array('name' => 'strlen')); 80 | $saved = $this->watchFunctions->getAll(); 81 | 82 | $_POST = array( 83 | 'watch' => array( 84 | array('removed' => 1, 'name' => 'strpos', '_id' => $saved[0]['_id']) 85 | ) 86 | ); 87 | $this->watches->post(); 88 | $result = $this->watchFunctions->getAll(); 89 | 90 | $this->assertCount(0, $result); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /tests/Db/MapperTest.php: -------------------------------------------------------------------------------- 1 | mapper = new Xhgui_Db_Mapper(); 9 | } 10 | 11 | public function testConvertConditions() 12 | { 13 | $opts = array( 14 | 'conditions' => array( 15 | 'simple_url' => '/tasks', 16 | 'url' => 'tasks', 17 | 'date_start' => '2013-01-20', 18 | 'date_end' => '2013-01-21', 19 | 'request_start' => '2013-02-13 12:22:00', 20 | 'request_end' => '2013-02-13 14:22:00', 21 | 'remote_addr' => '127.0.0.1', 22 | ) 23 | ); 24 | $result = $this->mapper->convert($opts); 25 | $expected = array( 26 | 'meta.simple_url' => '/tasks', 27 | 'meta.url' => array( 28 | '$regex' => 'tasks', 29 | '$options' => 'i' 30 | ), 31 | 'meta.request_date' => array( 32 | '$gte' => '2013-01-20', 33 | '$lte' => '2013-01-21' 34 | ), 35 | 'meta.SERVER.REQUEST_TIME' => array( 36 | '$gte' => strtotime($opts['conditions']['request_start']), 37 | '$lte' => strtotime($opts['conditions']['request_end']), 38 | ), 39 | 'meta.SERVER.REMOTE_ADDR' => '127.0.0.1', 40 | ); 41 | $this->assertEquals($expected, $result['conditions']); 42 | } 43 | 44 | public function testConvertConditionsLimit() 45 | { 46 | $opts = array( 47 | 'conditions' => array( 48 | 'simple_url' => '/tasks', 49 | 'limit' => 'P1D' 50 | ) 51 | ); 52 | $date = new DateTime(); 53 | $date->sub(new DateInterval('P1D')); 54 | 55 | $result = $this->mapper->convert($opts); 56 | $expected = array( 57 | 'meta.request_ts' => array( 58 | '$gte' => new MongoDate($date->getTimestamp()), 59 | ), 60 | 'meta.simple_url' => '/tasks' 61 | ); 62 | $this->assertEquals($expected, $result['conditions']); 63 | } 64 | 65 | public function testConvertConditionsLimitIgnoreDateStart() 66 | { 67 | $opts = array( 68 | 'conditions' => array( 69 | 'simple_url' => '/tasks', 70 | 'limit' => 'P1D', 71 | 'date_start' => '2013-10-16', 72 | ) 73 | ); 74 | $date = new DateTime(); 75 | $date->sub(new DateInterval('P1D')); 76 | 77 | $result = $this->mapper->convert($opts); 78 | $expected = array( 79 | 'meta.request_ts' => array( 80 | '$gte' => new MongoDate($date->getTimestamp()), 81 | ), 82 | 'meta.simple_url' => '/tasks' 83 | ); 84 | $this->assertEquals($expected, $result['conditions']); 85 | } 86 | 87 | public function testConditionsPartial() 88 | { 89 | $result = $this->mapper->convert(array( 90 | 'conditions' => array( 91 | 'date_start' => '2013-01-15', 92 | ) 93 | )); 94 | $expected = array( 95 | 'meta.request_date' => array( 96 | '$gte' => '2013-01-15', 97 | ) 98 | ); 99 | $this->assertEquals($expected, $result['conditions']); 100 | 101 | $result = $this->mapper->convert(array( 102 | 'conditions' => array( 103 | 'date_end' => '2013-01-20', 104 | ) 105 | )); 106 | $expected = array( 107 | 'meta.request_date' => array( 108 | '$lte' => '2013-01-20' 109 | ) 110 | ); 111 | $this->assertEquals($expected, $result['conditions']); 112 | 113 | $result = $this->mapper->convert(array( 114 | 'conditions' => array( 115 | 'date_start' => '2013-01-15', 116 | 'date_end' => '2013-01-20', 117 | 'url' => 'tasks' 118 | ) 119 | )); 120 | $expected = array( 121 | 'meta.url' => array( 122 | '$regex' => 'tasks', 123 | '$options' => 'i' 124 | ), 125 | 'meta.request_date' => array( 126 | '$gte' => '2013-01-15', 127 | '$lte' => '2013-01-20' 128 | ) 129 | ); 130 | $this->assertEquals($expected, $result['conditions']); 131 | } 132 | 133 | public function testConvertSort() 134 | { 135 | $options = array( 136 | 'sort' => 'time', 137 | ); 138 | $result = $this->mapper->convert($options); 139 | $this->assertEquals( 140 | array('meta.SERVER.REQUEST_TIME' => -1), 141 | $result['sort'] 142 | ); 143 | 144 | $options = array( 145 | 'sort' => 'wt', 146 | ); 147 | $result = $this->mapper->convert($options); 148 | $this->assertEquals( 149 | array('profile.main().wt' => -1), 150 | $result['sort'] 151 | ); 152 | 153 | $options = array( 154 | 'sort' => 'wt', 155 | 'direction' => 'asc' 156 | ); 157 | $result = $this->mapper->convert($options); 158 | $this->assertEquals( 159 | array('profile.main().wt' => 1), 160 | $result['sort'] 161 | ); 162 | $this->assertEquals('asc', $result['direction']); 163 | 164 | $options = array( 165 | 'sort' => 'wt', 166 | 'direction' => 'desc' 167 | ); 168 | $result = $this->mapper->convert($options); 169 | $this->assertEquals( 170 | array('profile.main().wt' => -1), 171 | $result['sort'] 172 | ); 173 | $this->assertEquals('desc', $result['direction']); 174 | 175 | $options = array( 176 | 'sort' => 'wt', 177 | 'direction' => 'farts' 178 | ); 179 | $result = $this->mapper->convert($options); 180 | $this->assertEquals( 181 | array('profile.main().wt' => -1), 182 | $result['sort'] 183 | ); 184 | $this->assertEquals('desc', $result['direction']); 185 | 186 | $options = array( 187 | 'sort' => 'barf', 188 | ); 189 | $result = $this->mapper->convert($options); 190 | $this->assertEquals( 191 | array('meta.SERVER.REQUEST_TIME' => -1), 192 | $result['sort'] 193 | ); 194 | } 195 | 196 | public function testConvertPerPage() 197 | { 198 | $options = array(); 199 | $result = $this->mapper->convert($options); 200 | $this->assertEquals(25, $result['perPage']); 201 | 202 | $options = array( 203 | 'perPage' => 1 204 | ); 205 | $result = $this->mapper->convert($options); 206 | $this->assertEquals(1, $result['perPage']); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /tests/ProfilesTest.php: -------------------------------------------------------------------------------- 1 | profiles = $di['profiles']; 8 | loadFixture($this->profiles, 'tests/fixtures/results.json'); 9 | } 10 | 11 | public function testPagination() 12 | { 13 | $options = array( 14 | 'page' => 1, 15 | 'sort' => 'wt', 16 | ); 17 | $result = $this->profiles->paginate($options); 18 | $this->assertEquals(25, $result['perPage'], 'default works'); 19 | $this->assertEquals(1, $result['page']); 20 | $this->assertEquals( 21 | array('profile.main().wt' => -1), 22 | $result['sort'] 23 | ); 24 | } 25 | 26 | public function testPaginateInvalidSort() 27 | { 28 | $options = array( 29 | 'page' => 1, 30 | 'sort' => 'barf', 31 | ); 32 | $result = $this->profiles->paginate($options); 33 | $this->assertEquals( 34 | array('meta.SERVER.REQUEST_TIME' => -1), 35 | $result['sort'] 36 | ); 37 | } 38 | 39 | public function testPaginateOutOfRangePage() 40 | { 41 | $options = array( 42 | 'page' => 9000, 43 | 'sort' => 'barf', 44 | ); 45 | $result = $this->profiles->paginate($options); 46 | $this->assertEquals(1, $result['page']); 47 | } 48 | 49 | public function testGetForUrl() 50 | { 51 | $options = array( 52 | 'perPage' => 1 53 | ); 54 | $result = $this->profiles->getForUrl('/', $options); 55 | $this->assertEquals(1, $result['page']); 56 | $this->assertEquals(2, $result['totalPages']); 57 | $this->assertEquals(1, $result['perPage']); 58 | 59 | $this->assertCount(1, $result['results']); 60 | $this->assertInstanceOf('Xhgui_Profile', $result['results'][0]); 61 | 62 | $result = $this->profiles->getForUrl('/not-there', $options); 63 | $this->assertCount(0, $result['results']); 64 | } 65 | 66 | public function testGetForUrlWithSearch() 67 | { 68 | $options = array( 69 | 'perPage' => 2 70 | ); 71 | $search = array( 72 | 'date_start' => '2013-01-17', 73 | 'date_end' => '2013-01-18', 74 | ); 75 | $result = $this->profiles->getForUrl('/', $options, $search); 76 | $this->assertEquals(1, $result['page']); 77 | $this->assertEquals(1, $result['totalPages']); 78 | $this->assertEquals(2, $result['perPage']); 79 | $this->assertCount(1, $result['results']); 80 | 81 | $search = array( 82 | 'date_start' => '2013-01-01', 83 | 'date_end' => '2013-01-02', 84 | ); 85 | $result = $this->profiles->getForUrl('/', $options, $search); 86 | $this->assertCount(0, $result['results']); 87 | } 88 | 89 | public function testGetAvgsForUrl() 90 | { 91 | $result = $this->profiles->getAvgsForUrl('/'); 92 | $this->assertCount(2, $result); 93 | 94 | $this->assertArrayHasKey('avg_wt', $result[0]); 95 | $this->assertArrayHasKey('avg_cpu', $result[0]); 96 | $this->assertArrayHasKey('avg_mu', $result[0]); 97 | $this->assertArrayHasKey('avg_pmu', $result[0]); 98 | 99 | $this->assertEquals('2013-01-18', $result[0]['date']); 100 | $this->assertEquals('2013-01-19', $result[1]['date']); 101 | } 102 | 103 | public function testGetAvgsForUrlWithSearch() 104 | { 105 | $search = array('date_start' => '2013-01-18', 'date_end' => '2013-01-18'); 106 | $result = $this->profiles->getAvgsForUrl('/', $search); 107 | $this->assertCount(1, $result); 108 | 109 | $this->assertArrayHasKey('avg_wt', $result[0]); 110 | $this->assertArrayHasKey('avg_cpu', $result[0]); 111 | $this->assertArrayHasKey('avg_mu', $result[0]); 112 | $this->assertArrayHasKey('avg_pmu', $result[0]); 113 | 114 | $this->assertEquals('2013-01-18', $result[0]['date']); 115 | } 116 | 117 | public function testGetPercentileForUrlWithSearch() 118 | { 119 | $search = array('date_start' => '2013-01-18', 'date_end' => '2013-01-18'); 120 | $result = $this->profiles->getPercentileForUrl(20, '/', $search); 121 | $this->assertCount(1, $result); 122 | 123 | $this->assertArrayHasKey('wt', $result[0]); 124 | $this->assertArrayHasKey('cpu', $result[0]); 125 | $this->assertArrayHasKey('mu', $result[0]); 126 | $this->assertArrayHasKey('pmu', $result[0]); 127 | } 128 | 129 | public function testGetPercentileForUrlWithLimit() 130 | { 131 | $search = array('limit' => 'P1D'); 132 | $result = $this->profiles->getPercentileForUrl(20, '/', $search); 133 | $this->assertCount(0, $result); 134 | } 135 | 136 | public function testGetAllConditions() 137 | { 138 | $result = $this->profiles->getAll(array( 139 | 'conditions' => array( 140 | 'date_start' => '2013-01-20', 141 | 'date_end' => '2013-01-21', 142 | 'url' => 'tasks', 143 | ) 144 | )); 145 | $this->assertEquals(1, $result['page']); 146 | $this->assertEquals(25, $result['perPage']); 147 | $this->assertEquals(1, $result['totalPages']); 148 | $this->assertCount(2, $result['results']); 149 | } 150 | 151 | public function testLatest() 152 | { 153 | $result = $this->profiles->latest(); 154 | $this->assertInstanceOf('Xhgui_Profile', $result); 155 | $this->assertEquals('2013-01-21', $result->getDate()->format('Y-m-d')); 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /tests/Saver/FileTest.php: -------------------------------------------------------------------------------- 1 | save($data); 13 | 14 | $this->assertEquals(json_encode($data).PHP_EOL, file_get_contents($file)); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tests/Saver/MongoTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('Xhgui_Profiles') 10 | ->disableOriginalConstructor() 11 | ->getMock(); 12 | $profiles->expects($this->once()) 13 | ->method('insert') 14 | ->with($this->equalTo($data)); 15 | 16 | $saver = new Xhgui_Saver_Mongo($profiles); 17 | $saver->save($data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Twig/ExtensionTest.php: -------------------------------------------------------------------------------- 1 | get('/test', function () {})->name('test'); 12 | $this->ext = new Xhgui_Twig_Extension($app); 13 | } 14 | 15 | public function testFormatBytes() 16 | { 17 | $result = $this->ext->formatBytes(2999); 18 | $expected = '2,999 bytes'; 19 | $this->assertEquals($expected, $result); 20 | } 21 | 22 | public function testFormatTime() 23 | { 24 | $result = $this->ext->formatTime(2999); 25 | $expected = '2,999 µs'; 26 | $this->assertEquals($expected, $result); 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function makePercentProvider() { 33 | return array( 34 | array( 35 | 10, 36 | 100, 37 | '10 %' 38 | ), 39 | array( 40 | 0.5, 41 | 100, 42 | '1 %' 43 | ), 44 | array( 45 | 100, 46 | 0, 47 | '0 %' 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * @dataProvider makePercentProvider 54 | */ 55 | public function testMakePercent($value, $total, $expected) 56 | { 57 | $result = $this->ext->makePercent($value, $total, $total); 58 | $this->assertEquals($expected, $result); 59 | } 60 | 61 | public static function urlProvider() 62 | { 63 | return array( 64 | // simple no query string 65 | array( 66 | 'test', 67 | null, 68 | '/test' 69 | ), 70 | // simple with query string 71 | array( 72 | 'test', 73 | array('test' => 'value'), 74 | '/test?test=value' 75 | ), 76 | ); 77 | } 78 | 79 | /** 80 | * @dataProvider urlProvider 81 | */ 82 | public function testUrl($url, $query, $expected) 83 | { 84 | $_SERVER['PHP_SELF'] = '/'; 85 | $_SERVER['REQUEST_METHOD'] = 'GET'; 86 | $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; 87 | $_SERVER['REQUEST_URI'] = '/'; 88 | $_SERVER['SERVER_NAME'] = 'localhost'; 89 | $_SERVER['SERVER_PORT'] = '80'; 90 | 91 | $result = $this->ext->url($url, $query); 92 | $this->assertStringEndsWith($expected, $result); 93 | } 94 | 95 | public function testStaticUrlNoIndexPhp() 96 | { 97 | Environment::mock(array( 98 | 'SCRIPT_NAME' => '/index.php', 99 | 'PHP_SELF' => '/index.php', 100 | 'REQUEST_URI' => '/', 101 | )); 102 | $result = $this->ext->staticUrl('css/bootstrap.css'); 103 | $this->assertEquals('/css/bootstrap.css', $result); 104 | } 105 | 106 | public function testStaticUrlWithIndexPhp() 107 | { 108 | Environment::mock(array( 109 | 'SCRIPT_NAME' => '/xhgui/webroot/index.php', 110 | 'PHP_SELF' => '/xhgui/webroot/index.php/', 111 | 'REQUEST_URI' => '/xhgui/webroot/index.php/', 112 | )); 113 | $result = $this->ext->staticUrl('css/bootstrap.css'); 114 | $this->assertEquals('/xhgui/webroot/css/bootstrap.css', $result); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/WatchFunctionsTest.php: -------------------------------------------------------------------------------- 1 | watch = $di['watchFunctions']; 9 | $this->watch->truncate(); 10 | } 11 | 12 | public function testSaveInsert() 13 | { 14 | $data = array( 15 | 'name' => 'strlen', 16 | ); 17 | $this->assertTrue($this->watch->save($data)); 18 | $this->assertCount(1, $this->watch->getAll()); 19 | 20 | $data = array( 21 | 'name' => 'empty', 22 | ); 23 | $this->assertTrue($this->watch->save($data)); 24 | $this->assertCount(2, $this->watch->getAll()); 25 | } 26 | 27 | public function testSaveUpdate() 28 | { 29 | $data = array( 30 | 'name' => 'strlen', 31 | ); 32 | $this->watch->save($data); 33 | $result = $this->watch->getAll(); 34 | 35 | $result[0]['name'] = 'strpos'; 36 | $this->assertTrue($this->watch->save($result[0])); 37 | $results = $this->watch->getAll(); 38 | $this->assertCount(1, $results); 39 | $this->assertEquals('strpos', $results[0]['name']); 40 | } 41 | 42 | public function testSaveRemove() 43 | { 44 | $data = array( 45 | 'name' => 'strlen', 46 | ); 47 | $this->watch->save($data); 48 | $result = $this->watch->getAll(); 49 | 50 | $result[0]['removed'] = 1; 51 | $this->assertTrue($this->watch->save($result[0])); 52 | $this->assertCount(0, $this->watch->getAll()); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | insert($record); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/fixtures/results.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "aaaaaaaaaaaaaaaaaaaaaaaa", 4 | "meta": { 5 | "url": "/tasks?page=3", 6 | "simple_url": "/tasks", 7 | "get": [], 8 | "env": [], 9 | "SERVER": {"REQUEST_TIME": 1358787612}, 10 | "request_date": "2013-01-21" 11 | }, 12 | "profile": { 13 | "main()==>strpos()": { 14 | "ct": 10, 15 | "wt": 600, 16 | "cpu": 500, 17 | "mu": 3500, 18 | "pmu": 3800 19 | }, 20 | "main()": { 21 | "ct": 1, 22 | "wt": 50139, 23 | "cpu": 49513, 24 | "mu": 3449360, 25 | "pmu": 3535120 26 | } 27 | } 28 | }, 29 | { 30 | "_id": "aaaaaaaaaaaaaaaaaaaaaaab", 31 | "meta": { 32 | "url": "/tasks?page=2", 33 | "simple_url": "/tasks", 34 | "get": [], 35 | "env": [], 36 | "SERVER": {"REQUEST_TIME": 1358701212}, 37 | "request_date": "2013-01-20" 38 | }, 39 | "profile": { 40 | "main()": { 41 | "ct": 1, 42 | "wt": 35, 43 | "cpu": 50, 44 | "mu": 3000, 45 | "pmu": 3000 46 | }, 47 | "main()==>eat_burger()": { 48 | "ct": 1, 49 | "wt": 25, 50 | "cpu": 20, 51 | "mu": 2000, 52 | "pmu": 2500 53 | }, 54 | "eat_burger()==>chew_food()": { 55 | "ct": 10, 56 | "wt": 22, 57 | "cpu": 18, 58 | "mu": 100, 59 | "pmu": 150 60 | }, 61 | "eat_burger()==>strlen()": { 62 | "ct": 1, 63 | "wt": 1, 64 | "cpu": 1, 65 | "mu": 50, 66 | "pmu": 50 67 | }, 68 | "main()==>drink_beer()": { 69 | "ct": 1, 70 | "wt": 14, 71 | "cpu": 10, 72 | "mu": 200, 73 | "pmu": 250 74 | }, 75 | "drink_beer()==>lift_glass()": { 76 | "ct": 5, 77 | "wt": 10, 78 | "cpu": 8, 79 | "mu": 10, 80 | "pmu": 15 81 | }, 82 | "drink_beer()==>strlen()": { 83 | "ct": 1, 84 | "wt": 1, 85 | "cpu": 1, 86 | "mu": 1, 87 | "pmu": 1 88 | } 89 | } 90 | }, 91 | { 92 | "_id": "aaaaaaaaaaaaaaaaaaaaaaac", 93 | "meta": { 94 | "url": "/tasks?page=1", 95 | "simple_url": "/tasks", 96 | "get": [], 97 | "env": [], 98 | "SERVER": {"REQUEST_TIME": 1358614812}, 99 | "request_date": "2013-01-19" 100 | }, 101 | "profile": { 102 | "main()": { 103 | "ct": 1, 104 | "wt": 50139, 105 | "cpu": 49513, 106 | "mu": 3449360, 107 | "pmu": 3535120 108 | }, 109 | "main()==>load_file()": { 110 | "ct": 1, 111 | "wt": 10000, 112 | "cpu": 10000, 113 | "mu": 35000, 114 | "pmu": 35000 115 | }, 116 | "main()==>parse_string()": { 117 | "ct": 1, 118 | "wt": 10000, 119 | "cpu": 10000, 120 | "mu": 35000, 121 | "pmu": 35000 122 | }, 123 | "load_file()==>open()": { 124 | "ct": 1, 125 | "wt": 5000, 126 | "cpu": 5000, 127 | "mu": 15000, 128 | "pmu": 15000 129 | }, 130 | "parse_string()==>open()": { 131 | "ct": 1, 132 | "wt": 5000, 133 | "cpu": 5000, 134 | "mu": 15000, 135 | "pmu": 15000 136 | }, 137 | "open()==>strlen()": { 138 | "ct": 1, 139 | "wt": 5000, 140 | "cpu": 5000, 141 | "mu": 1000, 142 | "pmu": 1000 143 | } 144 | } 145 | }, 146 | { 147 | "_id": "aaaaaaaaaaaaaaaaaaaaaaad", 148 | "meta": { 149 | "url": "/?page=1", 150 | "simple_url": "/", 151 | "get": [], 152 | "env": [], 153 | "SERVER": {"REQUEST_TIME": 1358528412}, 154 | "request_date": "2013-01-18" 155 | }, 156 | "profile": { 157 | "main()": { 158 | "ct": 1, 159 | "wt": 50139, 160 | "cpu": 49513, 161 | "mu": 3449360, 162 | "pmu": 3535120 163 | }, 164 | "strpos()": { 165 | "ct": 1, 166 | "wt": 10, 167 | "cpu": 12, 168 | "mu": 3000, 169 | "pmu": 3001 170 | } 171 | } 172 | }, 173 | { 174 | "_id": "aaaaaaaaaaaaaaaaaaaaaaae", 175 | "meta": { 176 | "url": "/?page=1", 177 | "simple_url": "/", 178 | "get": [], 179 | "env": [], 180 | "SERVER": {"REQUEST_TIME": 1358614812}, 181 | "request_date": "2013-01-19" 182 | }, 183 | "profile": { 184 | "main()": { 185 | "ct": 1, 186 | "wt": 60000, 187 | "cpu": 50000, 188 | "mu": 355999, 189 | "pmu": 375999 190 | } 191 | } 192 | } 193 | ] 194 | -------------------------------------------------------------------------------- /webroot/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule ^ index.php [QSA,L] 5 | 6 | -------------------------------------------------------------------------------- /webroot/css/d3.flameGraph.css: -------------------------------------------------------------------------------- 1 | .d3-flame-graph rect { 2 | stroke: #fff; 3 | fill-opacity: .8; 4 | } 5 | 6 | .d3-flame-graph .frame { 7 | cursor: pointer; 8 | } 9 | .d3-flame-graph .frame:hover .label { 10 | color: #fff; 11 | } 12 | 13 | .d3-flame-graph .label { 14 | pointer-events: none; 15 | white-space: nowrap; 16 | text-overflow: ellipsis; 17 | overflow: hidden; 18 | font-size: 12px; 19 | font-family: Verdana; 20 | margin-left: 4px; 21 | margin-right: 4px; 22 | line-height: 1.5; 23 | padding: 0 0 0; 24 | font-weight: 400; 25 | color: black; 26 | text-align: left; 27 | } 28 | 29 | .d3-flame-graph .fade { 30 | opacity: 0.6 !important; 31 | } 32 | 33 | .d3-flame-graph .title { 34 | font-size: 20px; 35 | } 36 | 37 | .d3-flame-graph .label { 38 | background: transparent; 39 | } 40 | 41 | .d3-flame-graph-tip { 42 | line-height: 1; 43 | padding: 12px; 44 | background: #fff; 45 | border-radius: 6px; 46 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 47 | border: 1px solid rgba(0, 0, 0, 0.2); 48 | color: #000; 49 | pointer-events: none; 50 | } 51 | 52 | /* Creates a small triangle extender for the tooltip */ 53 | .d3-flame-graph-tip:before, 54 | .d3-flame-graph-tip:after { 55 | pointer-events: none; 56 | content: ''; 57 | position: absolute; 58 | display: block; 59 | width: 0; 60 | height: 0; 61 | border-width: 11px; 62 | border-color: transparent; 63 | border-style: solid; 64 | left: 50%; 65 | z-index: 2; 66 | } 67 | .d3-flame-graph-tip:after { 68 | border-bottom-color: #fff; 69 | margin-left: -11px; 70 | top: -22px; 71 | z-index: 2; 72 | } 73 | .d3-flame-graph-tip:before { 74 | border-width: 12px; 75 | border-bottom-color: #999; 76 | margin-left: -12px; 77 | top: -24px; 78 | } 79 | -------------------------------------------------------------------------------- /webroot/css/datepicker.css: -------------------------------------------------------------------------------- 1 | /* 2 | Datepicker for Bootstrap 3 | Copyright 2012 Stefan Petre 4 | Licensed under the Apache License v2.0 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | */ 7 | .datepicker { 8 | top: 0; 9 | left: 0; 10 | padding: 4px; 11 | margin-top: 1px; 12 | -webkit-border-radius: 4px; 13 | -moz-border-radius: 4px; 14 | border-radius: 4px; 15 | } 16 | 17 | .datepicker:before { 18 | content: ''; 19 | display: inline-block; 20 | border-left: 7px solid transparent; 21 | border-right: 7px solid transparent; 22 | border-bottom: 7px solid #ccc; 23 | border-bottom-color: rgba(0,0,0,0.2); 24 | position: absolute; 25 | top: -7px; 26 | left: 6px; 27 | } 28 | 29 | .datepicker:after { 30 | content: ''; 31 | display: inline-block; 32 | border-left: 6px solid transparent; 33 | border-right: 6px solid transparent; 34 | border-bottom: 6px solid #fff; 35 | position: absolute; 36 | top: -6px; 37 | left: 7px; 38 | } 39 | 40 | .datepicker > div { 41 | display: none; 42 | } 43 | 44 | .datepicker table { 45 | width: 100%; 46 | margin: 0; 47 | } 48 | 49 | .datepicker td,.datepicker th { 50 | text-align: center; 51 | width: 20px; 52 | height: 20px; 53 | -webkit-border-radius: 4px; 54 | -moz-border-radius: 4px; 55 | border-radius: 4px; 56 | } 57 | 58 | .datepicker td.day:hover { 59 | background: #eee; 60 | cursor: pointer; 61 | } 62 | 63 | .datepicker td.old,.datepicker td.new { 64 | color: #999; 65 | } 66 | 67 | .datepicker td.active, 68 | .datepicker td.active:hover { 69 | background-color: #59add2; 70 | background-image: -moz-linear-gradient(top, #59bdd2, #5995d2); 71 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#59bdd2), to(#5995d2)); 72 | background-image: -webkit-linear-gradient(top, #59bdd2, #5995d2); 73 | background-image: -o-linear-gradient(top, #59bdd2, #5995d2); 74 | background-image: linear-gradient(to bottom, #59bdd2, #5995d2); 75 | text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25); 76 | color: #fff; 77 | } 78 | 79 | .datepicker td span { 80 | display: block; 81 | width: 47px; 82 | height: 54px; 83 | line-height: 54px; 84 | float: left; 85 | margin: 2px; 86 | cursor: pointer; 87 | -webkit-border-radius: 4px; 88 | -moz-border-radius: 4px; 89 | border-radius: 4px; 90 | } 91 | 92 | .datepicker td span:hover { 93 | background: #eee; 94 | } 95 | 96 | .datepicker td span.old { 97 | color: #999; 98 | } 99 | 100 | .datepicker th.switch { 101 | width: 145px; 102 | } 103 | 104 | .datepicker th.next, 105 | .datepicker th.prev { 106 | font-size: 19.5px; 107 | } 108 | 109 | .datepicker thead tr:first-child th { 110 | cursor: pointer; 111 | } 112 | 113 | .datepicker thead tr:first-child th:hover { 114 | background: #eee; 115 | } 116 | 117 | .input-append.date .add-on i, 118 | .input-prepend.date .add-on i { 119 | display: block; 120 | cursor: pointer; 121 | width: 16px; 122 | height: 16px; 123 | } 124 | -------------------------------------------------------------------------------- /webroot/css/xhgui.css: -------------------------------------------------------------------------------- 1 | /** 2 | * XHGui colors 3 | * 4 | * red - #d46245 5 | * orange - #e9814f 6 | * green - #637964 7 | * yellow - #ffe85e 8 | * blue - #59bdd2 9 | * pink - #e3b7b3 10 | * purple - #b63c71 11 | */ 12 | @font-face { 13 | font-family: 'Chunkfive'; 14 | src: url('../fonts/Chunkfive-webfont.eot'); 15 | src: url('../fonts/Chunkfive-webfont.eot?#iefix') format('embedded-opentype'), 16 | url('../fonts/Chunkfive-webfont.woff') format('woff'), 17 | url('../fonts/Chunkfive-webfont.ttf') format('truetype'), 18 | url('../fonts/Chunkfive-webfont.svg#MuseoSlab500') format('svg'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | /** 24 | * Grid extensions 25 | */ 26 | .row-spaced { 27 | margin-bottom: 20px; 28 | } 29 | 30 | /** 31 | * Typography 32 | */ 33 | h1, h2, h3 { 34 | margin: 15px 0; 35 | } 36 | h1, h2, h3, h4, h5, h6, 37 | .nav-header, 38 | .brand { 39 | -webkit-font-smoothing: antialiased; 40 | font-family: 'Chunkfive'; 41 | font-weight: normal; 42 | } 43 | .units { 44 | color: #999999; 45 | } 46 | .text-wrap { 47 | word-break: break-all; 48 | } 49 | 50 | .function-name { 51 | font-family: Consolas, Menlo, Courier, monospace; 52 | } 53 | 54 | /** 55 | * Top Navbar 56 | */ 57 | .navbar-inner { 58 | background: -webkit-linear-gradient(rgb(247, 247, 247), rgb(240, 240, 240)); 59 | background: -moz-linear-gradient(rgb(247, 247, 247), rgb(240, 240, 240)); 60 | background: linear-gradient(rgb(247, 247, 247), rgb(240, 240, 240)); 61 | } 62 | .navbar-static-top .navbar-inner { 63 | -webkit-box-shadow: none; 64 | -moz-box-shadow: none; 65 | box-shadow: none; 66 | } 67 | .navbar .brand { 68 | color: rgb(100, 100, 118); 69 | } 70 | .navbar .nav > li > a { 71 | color: #444; 72 | } 73 | .navbar .nav .current { 74 | background-color:#a7ef67; 75 | } 76 | .navbar .brand { 77 | border-right: 1px solid rgb(212, 212, 212); 78 | -webkit-box-shadow: 1px 0px 1px white; 79 | -moz-box-shadow: 1px 0px 1px white; 80 | box-shadow: 1px 0px 1px white; 81 | } 82 | .flash { 83 | margin-top: 20px; 84 | } 85 | 86 | /** 87 | * Sidebar navs 88 | */ 89 | .sidebar-nav { 90 | border-right: 1px solid rgb(221, 221, 221); 91 | } 92 | .nav-list dl { 93 | margin: 0; 94 | } 95 | .sidebar-nav li a { 96 | display: inline; 97 | padding: 0; 98 | margin: 0; 99 | } 100 | .sidebar-nav li a:hover { 101 | background: transparent; 102 | } 103 | 104 | /** 105 | * Search bar/box 106 | */ 107 | .searchbar { 108 | margin: 10px 0; 109 | padding: 10px; 110 | background: -webkit-linear-gradient(rgb(247, 247, 247), rgb(240, 240, 240)); 111 | background: -moz-linear-gradient(rgb(247, 247, 247), rgb(240, 240, 240)); 112 | background: linear-gradient(rgb(247, 247, 247), rgb(240, 240, 240)); 113 | -o-border-radius: 4px; 114 | -moz-border-radius: 4px; 115 | -webkit-border-radius: 4px; 116 | border-radius: 4px; 117 | } 118 | .searchbar form { 119 | margin-bottom: 0; 120 | } 121 | .searchbar .form-actions { 122 | margin: 0px; 123 | float: left; 124 | background: none; 125 | padding: 25px 0 0 0; 126 | border: none; 127 | } 128 | 129 | /** 130 | * Pagination 131 | * 132 | * Remove the borders and boxshadows bootstrap applies. 133 | */ 134 | .pagination ul { 135 | border: none; 136 | -moz-box-shadow: none; 137 | -webkit-box-shadow: none; 138 | box-shadow: none; 139 | } 140 | .pagination > ul > li.active > span { 141 | color: #d46245; 142 | } 143 | .pagination > ul > li > span, 144 | .pagination > ul > li > a { 145 | border-top: 0; 146 | border-bottom: 0; 147 | } 148 | .pagination ul > li:last-child > span, 149 | .pagination > ul > li:last-child > a { 150 | border-right: 0; 151 | } 152 | .pagination ul > li:first-child > span, 153 | .pagination > ul > li:first-child > a { 154 | border-left: 0; 155 | } 156 | 157 | /** 158 | * Tables 159 | */ 160 | th { 161 | text-align: center; 162 | } 163 | thead th { 164 | border-bottom: 1px solid #aaa; 165 | } 166 | /* Sort link colors are reversed */ 167 | thead .sort-link { 168 | color: #2f99af; 169 | } 170 | thead .sort-link:hover { 171 | color: #59bdd2; 172 | } 173 | th.right, 174 | td.right { 175 | text-align: right; 176 | } 177 | th.header { 178 | background: url(../img/bg.gif) center right no-repeat; 179 | cursor: pointer; 180 | padding-right: 18px; 181 | } 182 | .sort-desc, 183 | .sort-asc { 184 | background-repeat: no-repeat; 185 | background-position: center right; 186 | cursor: pointer; 187 | padding-right: 18px; 188 | } 189 | .sort-desc, 190 | th.headerSortDown { 191 | background-image: url(../img/desc.gif); 192 | } 193 | .sort-asc, 194 | th.headerSortUp { 195 | background-image: url(../img/asc.gif); 196 | } 197 | .tableFloatingHeader { 198 | background-color: #fff; 199 | border-bottom: 1px solid rgb(217, 217, 217); 200 | } 201 | .tableFloatingHeaderOriginal th { 202 | border-top: none; 203 | } 204 | tr .no-results { 205 | text-align: center; 206 | font-size: 1.2em; 207 | } 208 | 209 | 210 | .tooltip-inner { 211 | max-width: 400px; 212 | word-wrap: break-word; 213 | } 214 | 215 | 216 | /** 217 | * Watch lists 218 | */ 219 | .watch-list .btn { 220 | vertical-align: top; 221 | margin-top: 2px; 222 | display: none; 223 | } 224 | .watch-list li:hover .btn { 225 | display: inline-block; 226 | } 227 | 228 | /** 229 | * Chart styles 230 | */ 231 | .relative-box, 232 | .chart-container { 233 | position: relative; 234 | } 235 | .popover { 236 | min-width: 125px; 237 | width: auto; 238 | } 239 | 240 | .chart-axis path, 241 | .chart-axis line { 242 | fill: none; 243 | stroke: #666; 244 | shape-rendering: crispEdges; 245 | } 246 | .chart-line { 247 | fill: none; 248 | stroke: steelblue; 249 | stroke-width: 2px; 250 | } 251 | .chart-dots circle { 252 | stroke: white; 253 | stroke-width: 1.5px; 254 | } 255 | 256 | .chart-bar { 257 | fill: #d46245; 258 | } 259 | 260 | /** 261 | * Popover styles. 262 | */ 263 | .popover h5 { 264 | font-weight: bold; 265 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 266 | } 267 | .popover h5:first-child { 268 | margin-top: 0; 269 | } 270 | 271 | 272 | /** 273 | * Call graph chart 274 | */ 275 | 276 | /* Prototype graph styles. */ 277 | .callgraph { 278 | overflow: hidden; 279 | cursor: move; 280 | } 281 | .callgraph tspan, 282 | .callgraph text { 283 | user-select: none; 284 | -moz-user-select: none; 285 | -webkit-user-select: none; 286 | } 287 | 288 | .callgraph .node { 289 | cursor: pointer; 290 | } 291 | 292 | .callgraph .node rect { 293 | stroke: #333; 294 | stroke-width: 1.5px; 295 | fill: #fff; 296 | } 297 | .callgraph .active rect { 298 | stroke: #59bdd2; 299 | stroke-width: 4px; 300 | } 301 | 302 | .callgraph .edgeLabel rect { 303 | fill: #fff; 304 | } 305 | 306 | .callgraph .edgePath { 307 | stroke: #333; 308 | stroke-width: 1.5px; 309 | fill: none; 310 | } 311 | #metric-popover { 312 | left: auto; 313 | width: 250px; 314 | } 315 | #loading { 316 | text-align: center; 317 | margin: 20px; 318 | } 319 | #callgraph-details { 320 | width: 40%; 321 | height: 100%; 322 | position: fixed; 323 | top: 0; 324 | left: -40%; 325 | z-index: 1020; 326 | background: #fff; 327 | box-shadow: 0 4px 4px rgba(0, 0, 0, 0.5); 328 | transition: left 0.25s ease-out; 329 | } 330 | #callgraph-details.active { 331 | left: 0; 332 | transition: left 0.25s ease-out; 333 | } 334 | .details-content { 335 | height: 100%; 336 | padding: 0 10px 10px 10px; 337 | overflow-y: scroll; 338 | } 339 | .button-close { 340 | position: absolute; 341 | top: 5px; 342 | right: 5px; 343 | background: #888; 344 | color: #fff; 345 | display: inline-block; 346 | padding: 4px; 347 | line-height: 12px; 348 | font-size: 24px; 349 | height: 20px; 350 | width: 20px; 351 | border-radius: 15px; 352 | text-align: center; 353 | -moz-border-radius: 15px; 354 | -webkit-border-radius: 15px; 355 | vertical-align: middle; 356 | cursor: pointer; 357 | } 358 | 359 | /** 360 | * Waterfall chart 361 | */ 362 | .waterfall .axis { 363 | shape-rendering: crispEdges; 364 | } 365 | .waterfall .x.axis line { 366 | stroke: #ccc; 367 | } 368 | .waterfall .x.axis .minor { 369 | stroke-opacity: .5; 370 | } 371 | .waterfall .x.axis path { 372 | display: none; 373 | } 374 | .waterfall .bar { 375 | fill: #b63c71; 376 | } 377 | .waterfall text { 378 | color: black; 379 | } 380 | 381 | /** 382 | * Run details 383 | */ 384 | .back-link { 385 | float: right; 386 | margin-top: 20px; 387 | margin-left: 10px; 388 | } 389 | 390 | /** 391 | * Compare runs 392 | */ 393 | .compare-base { 394 | background: #e9814f; 395 | } 396 | .compare-head { 397 | background: #59bdd2; 398 | } 399 | .diff-up { 400 | color: #d46245; 401 | } 402 | .diff-down { 403 | color: #637964; 404 | } 405 | .diff-same { 406 | color: #999; 407 | } 408 | 409 | /** 410 | * Footer 411 | */ 412 | .footer-text { 413 | text-align: center; 414 | } 415 | -------------------------------------------------------------------------------- /webroot/fonts/Chunkfive-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/fonts/Chunkfive-webfont.eot -------------------------------------------------------------------------------- /webroot/fonts/Chunkfive-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/fonts/Chunkfive-webfont.ttf -------------------------------------------------------------------------------- /webroot/fonts/Chunkfive-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/fonts/Chunkfive-webfont.woff -------------------------------------------------------------------------------- /webroot/fonts/Open Font License.markdown: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Meredith Mandel , with Reserved Font Name: "Chunk". 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | Version 1.1 - 26 February 2007 8 | 9 | 10 | SIL Open Font License 11 | ==================================================== 12 | 13 | 14 | Preamble 15 | ---------- 16 | 17 | The goals of the Open Font License (OFL) are to stimulate worldwide 18 | development of collaborative font projects, to support the font creation 19 | efforts of academic and linguistic communities, and to provide a free and 20 | open framework in which fonts may be shared and improved in partnership 21 | with others. 22 | 23 | The OFL allows the licensed fonts to be used, studied, modified and 24 | redistributed freely as long as they are not sold by themselves. The 25 | fonts, including any derivative works, can be bundled, embedded, 26 | redistributed and/or sold with any software provided that any reserved 27 | names are not used by derivative works. The fonts and derivatives, 28 | however, cannot be released under any other type of license. The 29 | requirement for fonts to remain under this license does not apply 30 | to any document created using the fonts or their derivatives. 31 | 32 | Definitions 33 | ------------- 34 | 35 | `"Font Software"` refers to the set of files released by the Copyright 36 | Holder(s) under this license and clearly marked as such. This may 37 | include source files, build scripts and documentation. 38 | 39 | `"Reserved Font Name"` refers to any names specified as such after the 40 | copyright statement(s). 41 | 42 | `"Original Version"` refers to the collection of Font Software components as 43 | distributed by the Copyright Holder(s). 44 | 45 | `"Modified Version"` refers to any derivative made by adding to, deleting, 46 | or substituting -- in part or in whole -- any of the components of the 47 | Original Version, by changing formats or by porting the Font Software to a 48 | new environment. 49 | 50 | `"Author"` refers to any designer, engineer, programmer, technical 51 | writer or other person who contributed to the Font Software. 52 | 53 | Permission & Conditions 54 | ------------------------ 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining 57 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 58 | redistribute, and sell modified and unmodified copies of the Font 59 | Software, subject to the following conditions: 60 | 61 | 1. Neither the Font Software nor any of its individual components, 62 | in Original or Modified Versions, may be sold by itself. 63 | 64 | 2. Original or Modified Versions of the Font Software may be bundled, 65 | redistributed and/or sold with any software, provided that each copy 66 | contains the above copyright notice and this license. These can be 67 | included either as stand-alone text files, human-readable headers or 68 | in the appropriate machine-readable metadata fields within text or 69 | binary files as long as those fields can be easily viewed by the user. 70 | 71 | 3. No Modified Version of the Font Software may use the Reserved Font 72 | Name(s) unless explicit written permission is granted by the corresponding 73 | Copyright Holder. This restriction only applies to the primary font name as 74 | presented to the users. 75 | 76 | 4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font 77 | Software shall not be used to promote, endorse or advertise any 78 | Modified Version, except to acknowledge the contribution(s) of the 79 | Copyright Holder(s) and the Author(s) or with their explicit written 80 | permission. 81 | 82 | 5. The Font Software, modified or unmodified, in part or in whole, 83 | must be distributed entirely under this license, and must not be 84 | distributed under any other license. The requirement for fonts to 85 | remain under this license does not apply to any document created 86 | using the Font Software. 87 | 88 | Termination 89 | ----------- 90 | 91 | This license becomes null and void if any of the above conditions are 92 | not met. 93 | 94 | 95 | DISCLAIMER 96 | 97 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 98 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 99 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 100 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 101 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 102 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 103 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 104 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 105 | OTHER DEALINGS IN THE FONT SOFTWARE. 106 | -------------------------------------------------------------------------------- /webroot/img/asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/img/asc.gif -------------------------------------------------------------------------------- /webroot/img/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/img/bg.gif -------------------------------------------------------------------------------- /webroot/img/desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/img/desc.gif -------------------------------------------------------------------------------- /webroot/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /webroot/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /webroot/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/img/icon.png -------------------------------------------------------------------------------- /webroot/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/img/loading.gif -------------------------------------------------------------------------------- /webroot/index.php: -------------------------------------------------------------------------------- 1 | run(); 12 | -------------------------------------------------------------------------------- /webroot/js/bootstrap-tooltip.js: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | * bootstrap-tooltip.js v2.2.1 3 | * http://twitter.github.com/bootstrap/javascript.html#tooltips 4 | * Inspired by the original jQuery.tipsy by Jason Frame 5 | * =========================================================== 6 | * Copyright 2012 Twitter, Inc. 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * ========================================================== */ 20 | 21 | 22 | !function ($) { 23 | 24 | "use strict"; // jshint ;_; 25 | 26 | 27 | /* TOOLTIP PUBLIC CLASS DEFINITION 28 | * =============================== */ 29 | 30 | var Tooltip = function (element, options) { 31 | this.init('tooltip', element, options) 32 | } 33 | 34 | Tooltip.prototype = { 35 | 36 | constructor: Tooltip 37 | 38 | , init: function (type, element, options) { 39 | var eventIn 40 | , eventOut 41 | 42 | this.type = type 43 | this.$element = $(element) 44 | this.options = this.getOptions(options) 45 | this.enabled = true 46 | 47 | if (this.options.trigger == 'click') { 48 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) 49 | } else if (this.options.trigger != 'manual') { 50 | eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus' 51 | eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur' 52 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) 53 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) 54 | } 55 | 56 | this.options.selector ? 57 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : 58 | this.fixTitle() 59 | } 60 | 61 | , getOptions: function (options) { 62 | options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data()) 63 | 64 | if (options.delay && typeof options.delay == 'number') { 65 | options.delay = { 66 | show: options.delay 67 | , hide: options.delay 68 | } 69 | } 70 | 71 | return options 72 | } 73 | 74 | , enter: function (e) { 75 | var self = $(e.currentTarget)[this.type](this._options).data(this.type) 76 | 77 | if (!self.options.delay || !self.options.delay.show) return self.show() 78 | 79 | clearTimeout(this.timeout) 80 | self.hoverState = 'in' 81 | this.timeout = setTimeout(function() { 82 | if (self.hoverState == 'in') self.show() 83 | }, self.options.delay.show) 84 | } 85 | 86 | , leave: function (e) { 87 | var self = $(e.currentTarget)[this.type](this._options).data(this.type) 88 | 89 | if (this.timeout) clearTimeout(this.timeout) 90 | if (!self.options.delay || !self.options.delay.hide) return self.hide() 91 | 92 | self.hoverState = 'out' 93 | this.timeout = setTimeout(function() { 94 | if (self.hoverState == 'out') self.hide() 95 | }, self.options.delay.hide) 96 | } 97 | 98 | , show: function () { 99 | var $tip 100 | , inside 101 | , pos 102 | , actualWidth 103 | , actualHeight 104 | , placement 105 | , tp 106 | 107 | if (this.hasContent() && this.enabled) { 108 | $tip = this.tip() 109 | this.setContent() 110 | 111 | if (this.options.animation) { 112 | $tip.addClass('fade') 113 | } 114 | 115 | placement = typeof this.options.placement == 'function' ? 116 | this.options.placement.call(this, $tip[0], this.$element[0]) : 117 | this.options.placement 118 | 119 | inside = /in/.test(placement) 120 | 121 | $tip 122 | .detach() 123 | .css({ top: 0, left: 0, display: 'block' }) 124 | .insertAfter(this.$element) 125 | 126 | pos = this.getPosition(inside) 127 | 128 | actualWidth = $tip[0].offsetWidth 129 | actualHeight = $tip[0].offsetHeight 130 | 131 | switch (inside ? placement.split(' ')[1] : placement) { 132 | case 'bottom': 133 | tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} 134 | break 135 | case 'top': 136 | tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} 137 | break 138 | case 'left': 139 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} 140 | break 141 | case 'right': 142 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} 143 | break 144 | } 145 | 146 | $tip 147 | .offset(tp) 148 | .addClass(placement) 149 | .addClass('in') 150 | } 151 | } 152 | 153 | , setContent: function () { 154 | var $tip = this.tip() 155 | , title = this.getTitle() 156 | 157 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) 158 | $tip.removeClass('fade in top bottom left right') 159 | } 160 | 161 | , hide: function () { 162 | var that = this 163 | , $tip = this.tip() 164 | 165 | $tip.removeClass('in') 166 | 167 | function removeWithAnimation() { 168 | var timeout = setTimeout(function () { 169 | $tip.off($.support.transition.end).detach() 170 | }, 500) 171 | 172 | $tip.one($.support.transition.end, function () { 173 | clearTimeout(timeout) 174 | $tip.detach() 175 | }) 176 | } 177 | 178 | $.support.transition && this.$tip.hasClass('fade') ? 179 | removeWithAnimation() : 180 | $tip.detach() 181 | 182 | return this 183 | } 184 | 185 | , fixTitle: function () { 186 | var $e = this.$element 187 | if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { 188 | $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title') 189 | } 190 | } 191 | 192 | , hasContent: function () { 193 | return this.getTitle() 194 | } 195 | 196 | , getPosition: function (inside) { 197 | return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), { 198 | width: this.$element[0].offsetWidth 199 | , height: this.$element[0].offsetHeight 200 | }) 201 | } 202 | 203 | , getTitle: function () { 204 | var title 205 | , $e = this.$element 206 | , o = this.options 207 | 208 | title = $e.attr('data-original-title') 209 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 210 | 211 | return title 212 | } 213 | 214 | , tip: function () { 215 | return this.$tip = this.$tip || $(this.options.template) 216 | } 217 | 218 | , validate: function () { 219 | if (!this.$element[0].parentNode) { 220 | this.hide() 221 | this.$element = null 222 | this.options = null 223 | } 224 | } 225 | 226 | , enable: function () { 227 | this.enabled = true 228 | } 229 | 230 | , disable: function () { 231 | this.enabled = false 232 | } 233 | 234 | , toggleEnabled: function () { 235 | this.enabled = !this.enabled 236 | } 237 | 238 | , toggle: function (e) { 239 | var self = $(e.currentTarget)[this.type](this._options).data(this.type) 240 | self[self.tip().hasClass('in') ? 'hide' : 'show']() 241 | } 242 | 243 | , destroy: function () { 244 | this.hide().$element.off('.' + this.type).removeData(this.type) 245 | } 246 | 247 | } 248 | 249 | 250 | /* TOOLTIP PLUGIN DEFINITION 251 | * ========================= */ 252 | 253 | $.fn.tooltip = function ( option ) { 254 | return this.each(function () { 255 | var $this = $(this) 256 | , data = $this.data('tooltip') 257 | , options = typeof option == 'object' && option 258 | if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) 259 | if (typeof option == 'string') data[option]() 260 | }) 261 | } 262 | 263 | $.fn.tooltip.Constructor = Tooltip 264 | 265 | $.fn.tooltip.defaults = { 266 | animation: true 267 | , placement: 'top' 268 | , selector: false 269 | , template: '
' 270 | , trigger: 'hover' 271 | , title: '' 272 | , delay: 0 273 | , html: false 274 | } 275 | 276 | }(window.jQuery); 277 | -------------------------------------------------------------------------------- /webroot/js/jquery.stickytableheaders.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2011 by Jonas Mosbech - https://github.com/jmosbech/StickyTableHeaders 2 | MIT license info: https://github.com/jmosbech/StickyTableHeaders/blob/master/license.txt */ 3 | 4 | (function ($) { 5 | $.StickyTableHeaders = function (el, options) { 6 | // To avoid scope issues, use 'base' instead of 'this' 7 | // to reference this class from internal events and functions. 8 | var base = this; 9 | 10 | // Access to jQuery and DOM versions of element 11 | base.$el = $(el); 12 | base.el = el; 13 | 14 | // Add a reverse reference to the DOM object 15 | base.$el.data('StickyTableHeaders', base); 16 | 17 | base.init = function () { 18 | base.options = $.extend({}, $.StickyTableHeaders.defaultOptions, options); 19 | 20 | base.$el.each(function () { 21 | var $this = $(this); 22 | $this.wrap('
'); 23 | 24 | var originalHeaderRow = $('tr:first', this); 25 | originalHeaderRow.before(originalHeaderRow.clone()); 26 | var clonedHeaderRow = $('tr:first', this); 27 | 28 | clonedHeaderRow.addClass('tableFloatingHeader'); 29 | clonedHeaderRow.css('position', 'fixed'); 30 | clonedHeaderRow.css('top', '0px'); 31 | clonedHeaderRow.css('left', $this.css('margin-left')); 32 | clonedHeaderRow.css('display', 'none'); 33 | 34 | originalHeaderRow.addClass('tableFloatingHeaderOriginal'); 35 | 36 | // enabling support for jquery.tablesorter plugin 37 | $this.bind('sortEnd', function (e) { base.updateCloneFromOriginal(originalHeaderRow, clonedHeaderRow); }); 38 | }); 39 | 40 | base.updateTableHeaders(); 41 | $(window).scroll(base.updateTableHeaders); 42 | $(window).resize(base.updateTableHeaders); 43 | }; 44 | 45 | base.updateTableHeaders = function () { 46 | base.$el.each(function () { 47 | var $this = $(this); 48 | var $window = $(window); 49 | 50 | var fixedHeaderHeight = isNaN(base.options.fixedOffset) ? base.options.fixedOffset.height() : base.options.fixedOffset; 51 | 52 | var originalHeaderRow = $('.tableFloatingHeaderOriginal', this); 53 | var floatingHeaderRow = $('.tableFloatingHeader', this); 54 | var offset = $this.offset(); 55 | var scrollTop = $window.scrollTop() + fixedHeaderHeight; 56 | var scrollLeft = $window.scrollLeft(); 57 | 58 | if ((scrollTop > offset.top) && (scrollTop < offset.top + $this.height())) { 59 | floatingHeaderRow.css('top', fixedHeaderHeight + 'px'); 60 | floatingHeaderRow.css('margin-top', 0); 61 | floatingHeaderRow.css('left', (offset.left - scrollLeft) + 'px'); 62 | floatingHeaderRow.css('display', 'block'); 63 | 64 | base.updateCloneFromOriginal(originalHeaderRow, floatingHeaderRow); 65 | } 66 | else { 67 | floatingHeaderRow.css('display', 'none'); 68 | } 69 | }); 70 | }; 71 | 72 | base.updateCloneFromOriginal = function (originalHeaderRow, floatingHeaderRow) { 73 | // Copy cell widths and classes from original header 74 | $('th', floatingHeaderRow).each(function (index) { 75 | $this = $(this); 76 | var origCell = $('th', originalHeaderRow).eq(index); 77 | $this.removeClass().addClass(origCell.attr('class')); 78 | $this.css('width', origCell.width()); 79 | }); 80 | 81 | // Copy row width from whole table 82 | floatingHeaderRow.css('width', originalHeaderRow.width()); 83 | }; 84 | 85 | // Run initializer 86 | base.init(); 87 | }; 88 | 89 | $.StickyTableHeaders.defaultOptions = { 90 | fixedOffset: 0 91 | }; 92 | 93 | $.fn.stickyTableHeaders = function (options) { 94 | return this.each(function () { 95 | (new $.StickyTableHeaders(this, options)); 96 | }); 97 | }; 98 | 99 | })(jQuery); 100 | -------------------------------------------------------------------------------- /webroot/js/need/laydate.css: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @Name: laydate 核心样式 4 | @Author:贤心 5 | @Site:http://sentsin.com/layui/laydate 6 | 7 | **/ 8 | 9 | html{_background-image:url(about:blank); _background-attachment:fixed;} 10 | .laydate_body .laydate_box, .laydate_body .laydate_box *{margin:0; padding:0;} 11 | .laydate-icon, 12 | .laydate-icon-default, 13 | .laydate-icon-danlan, 14 | .laydate-icon-dahong, 15 | .laydate-icon-molv{height:22px; line-height:22px; padding-right:20px; border:1px solid #C6C6C6; background-repeat:no-repeat; background-position:right center; background-color:#fff; outline:0;} 16 | .laydate-icon-default{ background-image:url(../skins/default/icon.png)} 17 | .laydate-icon-dahong{background-image:url(../skins/dahong/icon.png)} 18 | .laydate-icon-molv{background-image:url(../skins/molv/icon.png)} 19 | .laydate_body .laydate_box{width:240px; font:12px '\5B8B\4F53'; z-index:99999999; *margin:-2px 0 0 -2px; *overflow:hidden; _margin:0; _position:absolute!important; background-color:#fff;} 20 | .laydate_body .laydate_box li{list-style:none;} 21 | .laydate_body .laydate_box .laydate_void{cursor:text!important;} 22 | .laydate_body .laydate_box a, .laydate_body .laydate_box a:hover{text-decoration:none; blr:expression(this.onFocus=this.blur()); cursor:pointer;} 23 | .laydate_body .laydate_box a:hover{text-decoration:none;} 24 | .laydate_body .laydate_box cite, .laydate_body .laydate_box label{position:absolute; width:0; height:0; border-width:5px; border-style:dashed; border-color:transparent; overflow:hidden; cursor:pointer;} 25 | .laydate_body .laydate_box .laydate_yms, .laydate_body .laydate_box .laydate_time{display:none;} 26 | .laydate_body .laydate_box .laydate_show{display:block;} 27 | .laydate_body .laydate_box input{outline:0; font-size:14px; background-color:#fff;} 28 | .laydate_body .laydate_top{position:relative; height:26px; padding:5px; *width:100%; z-index:99;} 29 | .laydate_body .laydate_ym{position:relative; float:left; height:24px; cursor:pointer;} 30 | .laydate_body .laydate_ym input{float:left; height:24px; line-height:24px; text-align:center; border:none; cursor:pointer;} 31 | .laydate_body .laydate_ym .laydate_yms{position:absolute; left: -1px; top: 24px; height:181px;} 32 | .laydate_body .laydate_y{width:121px; margin-right:6px;} 33 | .laydate_body .laydate_y input{width:64px; margin-right:15px;} 34 | .laydate_body .laydate_y .laydate_yms{width:121px; text-align:center;} 35 | .laydate_body .laydate_y .laydate_yms a{position:relative; display:block; height:20px;} 36 | .laydate_body .laydate_y .laydate_yms ul{height:139px; padding:0; *overflow:hidden;} 37 | .laydate_body .laydate_y .laydate_yms ul li{float:left; width:60px; height:20px; line-height: 20px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;} 38 | .laydate_body .laydate_m{width:99px;} 39 | .laydate_body .laydate_m .laydate_yms{width:99px; padding:0;} 40 | .laydate_body .laydate_m input{width:42px; margin-right:15px;} 41 | .laydate_body .laydate_m .laydate_yms span{display:block; float:left; width:42px; margin: 5px 0 0 5px; line-height:24px; text-align:center; _display:inline;} 42 | .laydate_body .laydate_choose{display:block; float:left; position:relative; width:20px; height:24px;} 43 | .laydate_body .laydate_choose cite, .laydate_body .laydate_tab cite{left:50%; top:50%;} 44 | .laydate_body .laydate_chtop cite{margin:-7px 0 0 -5px; border-bottom-style:solid;} 45 | .laydate_body .laydate_chdown cite, .laydate_body .laydate_ym label{top:50%; margin:-2px 0 0 -5px; border-top-style:solid;} 46 | .laydate_body .laydate_chprev cite{margin:-5px 0 0 -7px;} 47 | .laydate_body .laydate_chnext cite{margin:-5px 0 0 -2px;} 48 | .laydate_body .laydate_ym label{right:28px;} 49 | .laydate_body .laydate_table{ width:230px; margin:0 5px; border-collapse:collapse; border-spacing:0px; } 50 | .laydate_body .laydate_table td{width:31px; height:19px; line-height:19px; text-align: center; cursor:pointer; font-size: 12px;} 51 | .laydate_body .laydate_table thead{height:22px; line-height:22px;} 52 | .laydate_body .laydate_table thead th{font-weight:400; font-size:12px; text-align:center;} 53 | .laydate_body .laydate_bottom{position:relative; height:22px; line-height:20px; padding:5px; font-size:12px;} 54 | .laydate_body .laydate_bottom #laydate_hms{position: relative; z-index: 1; float:left; } 55 | .laydate_body .laydate_time{ position:absolute; left:5px; bottom: 26px; width:129px; height:125px; *overflow:hidden;} 56 | .laydate_body .laydate_time .laydate_hmsno{ padding:5px 0 0 5px;} 57 | .laydate_body .laydate_time .laydate_hmsno span{display:block; float:left; width:24px; height:19px; line-height:19px; text-align:center; cursor:pointer; *margin-bottom:-5px;} 58 | .laydate_body .laydate_time1{width:228px; height:154px;} 59 | .laydate_body .laydate_time1 .laydate_hmsno{padding: 6px 0 0 8px;} 60 | .laydate_body .laydate_time1 .laydate_hmsno span{width:21px; height:20px; line-height:20px;} 61 | .laydate_body .laydate_msg{left:49px; bottom:67px; width:141px; height:auto; overflow: hidden;} 62 | .laydate_body .laydate_msg p{padding:5px 10px;} 63 | .laydate_body .laydate_bottom li{float:left; height:20px; line-height:20px; border-right:none; font-weight:900;} 64 | .laydate_body .laydate_bottom .laydate_sj{width:33px; text-align:center; font-weight:400;} 65 | .laydate_body .laydate_bottom input{float:left; width:21px; height:20px; line-height:20px; border:none; text-align:center; cursor:pointer; font-size:12px; font-weight:400;} 66 | .laydate_body .laydate_bottom .laydte_hsmtex{height:20px; line-height:20px; text-align:center;} 67 | .laydate_body .laydate_bottom .laydte_hsmtex span{position:absolute; width:20px; top:0; right:0px; cursor:pointer;} 68 | .laydate_body .laydate_bottom .laydte_hsmtex span:hover{font-size:14px;} 69 | .laydate_body .laydate_bottom .laydate_btn{position:absolute; right:5px; top:5px;} 70 | .laydate_body .laydate_bottom .laydate_btn a{float:left; height:20px; padding:0 6px; _padding:0 5px;} 71 | .laydate_body .laydate_bottom .laydate_v{position:absolute; left:10px; top:6px; font-family:Courier; z-index:0;} 72 | 73 | -------------------------------------------------------------------------------- /webroot/js/skins/dahong/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/js/skins/dahong/icon.png -------------------------------------------------------------------------------- /webroot/js/skins/dahong/laydate.css: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @Name: laydate皮肤:大红 4 | @Author:贤心 5 | @Site:http://sentsin.com/layui/laydate 6 | 7 | **/ 8 | 9 | .laydate-icon{border:1px solid #ccc; background-image:url(icon.png)} 10 | 11 | .laydate_body .laydate_bottom #laydate_hms, 12 | .laydate_body .laydate_time{border:1px solid #ccc;} 13 | 14 | .laydate_body .laydate_box, 15 | .laydate_body .laydate_time{box-shadow: 2px 2px 5px rgba(0,0,0,.1);} 16 | 17 | .laydate_body .laydate_box{border-top:none; border-bottom:none; background-color:#fff; color:#333;} 18 | .laydate_body .laydate_box input{background:none!important; color:#fff;} 19 | .laydate_body .laydate_box .laydate_void{color:#ccc!important;} 20 | .laydate_body .laydate_box a, .laydate_body .laydate_box a:hover{color:#333;} 21 | .laydate_body .laydate_box a:hover{color:#666;} 22 | .laydate_body .laydate_click{background-color:#F32043!important; color:#fff!important;} 23 | .laydate_body .laydate_top{border-top:1px solid #D91600; background-color:#D91600} 24 | .laydate_body .laydate_ym{border:1px solid #D91600; background-color:#D91600;} 25 | .laydate_body .laydate_ym .laydate_yms{border:1px solid #D91600; background-color:#D91600; color:#fff;} 26 | .laydate_body .laydate_y .laydate_yms a{border-bottom:1px solid #D91600;} 27 | .laydate_body .laydate_y .laydate_yms .laydate_chdown{border-top:1px solid #D91600; border-bottom:none;} 28 | .laydate_body .laydate_choose{border-left:1px solid #D91600;} 29 | .laydate_body .laydate_chprev{border-left:none; border-right:1px solid #D91600;} 30 | .laydate_body .laydate_choose:hover, 31 | .laydate_body .laydate_y .laydate_yms a:hover{background-color:#F54766;} 32 | .laydate_body .laydate_chtop cite{border-bottom-color:#fff;} 33 | .laydate_body .laydate_chdown cite, .laydate_body .laydate_ym label{border-top-color:#fff;} 34 | .laydate_body .laydate_chprev cite{border-right-style:solid; border-right-color:#fff;} 35 | .laydate_body .laydate_chnext cite{border-left-style:solid; border-left-color:#fff;} 36 | .laydate_body .laydate_table{width: 240px!important; margin: 0!important; border:1px solid #ccc; border-top:none; border-bottom:none;} 37 | .laydate_body .laydate_table td{border:none; height:21px!important; line-height:21px!important; background-color:#fff; color:#333;} 38 | .laydate_body .laydate_table .laydate_nothis{color:#999;} 39 | .laydate_body .laydate_table thead{border-bottom:1px solid #ccc; height:21px!important; line-height:21px!important;} 40 | .laydate_body .laydate_table thead th{} 41 | .laydate_body .laydate_bottom{border:1px solid #ccc; border-top:none;} 42 | .laydate_body .laydate_bottom #laydate_hms{background-color:#fff;} 43 | .laydate_body .laydate_time{background-color:#fff;} 44 | .laydate_body .laydate_time1{width: 226px!important; height: 152px!important;} 45 | .laydate_body .laydate_bottom .laydate_sj{width:31px!important; border-right:1px solid #ccc; background-color:#fff;} 46 | .laydate_body .laydate_bottom input{background-color:#fff; color:#333;} 47 | .laydate_body .laydate_bottom .laydte_hsmtex{border-bottom:1px solid #ccc;} 48 | .laydate_body .laydate_bottom .laydate_btn{border-right:1px solid #ccc;} 49 | .laydate_body .laydate_bottom .laydate_v{color:#999} 50 | .laydate_body .laydate_bottom .laydate_btn a{border: 1px solid #ccc; border-right:none; background-color:#fff;} 51 | .laydate_body .laydate_bottom .laydate_btn a:hover{background-color:#F6F6F6; color:#333;} 52 | 53 | .laydate_body .laydate_m .laydate_yms span:hover, 54 | .laydate_body .laydate_time .laydate_hmsno span:hover, 55 | .laydate_body .laydate_y .laydate_yms ul li:hover, 56 | .laydate_body .laydate_table td:hover{background-color:#F54766; color:#fff;} 57 | 58 | 59 | -------------------------------------------------------------------------------- /webroot/js/skins/default/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/js/skins/default/icon.png -------------------------------------------------------------------------------- /webroot/js/skins/default/laydate.css: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @Name: laydate皮肤:默认 4 | @Author:贤心 5 | @Site:http://sentsin.com/layui/laydate 6 | 7 | **/ 8 | 9 | .laydate-icon{border:1px solid #C6C6C6; background-image:url(icon.png)} 10 | 11 | .laydate_body .laydate_box, 12 | .laydate_body .laydate_ym, 13 | .laydate_body .laydate_ym .laydate_yms, 14 | .laydate_body .laydate_table, 15 | .laydate_body .laydate_table td, 16 | .laydate_body .laydate_bottom #laydate_hms, 17 | .laydate_body .laydate_time, 18 | .laydate_body .laydate_bottom .laydate_btn a{border:1px solid #ccc;} 19 | 20 | .laydate_body .laydate_y .laydate_yms a, 21 | .laydate_body .laydate_choose, 22 | .laydate_body .laydate_table thead, 23 | .laydate_body .laydate_bottom .laydte_hsmtex{background-color:#F6F6F6;} 24 | 25 | .laydate_body .laydate_box, 26 | .laydate_body .laydate_ym .laydate_yms, 27 | .laydate_body .laydate_time{box-shadow: 2px 2px 5px rgba(0,0,0,.1);} 28 | 29 | .laydate_body .laydate_box{border-top:none; border-bottom:none; background-color:#fff; color:#333;} 30 | .laydate_body .laydate_box input{color:#333;} 31 | .laydate_body .laydate_box .laydate_void{color:#ccc!important; /*text-decoration:line-through;*/} 32 | .laydate_body .laydate_box .laydate_void:hover{background-color:#fff!important} 33 | .laydate_body .laydate_box a, .laydate_body .laydate_box a:hover{color:#333;} 34 | .laydate_body .laydate_box a:hover{color:#666;} 35 | .laydate_body .laydate_click{background-color:#eee!important;} 36 | .laydate_body .laydate_top{border-top:1px solid #C6C6C6;} 37 | .laydate_body .laydate_ym .laydate_yms{border:1px solid #C6C6C6; background-color:#fff;} 38 | .laydate_body .laydate_y .laydate_yms a{border-bottom:1px solid #C6C6C6;} 39 | .laydate_body .laydate_y .laydate_yms .laydate_chdown{border-top:1px solid #C6C6C6; border-bottom:none;} 40 | .laydate_body .laydate_choose{border-left:1px solid #C6C6C6;} 41 | .laydate_body .laydate_chprev{border-left:none; border-right:1px solid #C6C6C6;} 42 | .laydate_body .laydate_choose:hover, 43 | .laydate_body .laydate_y .laydate_yms a:hover{background-color:#fff;} 44 | .laydate_body .laydate_chtop cite{border-bottom-color:#666;} 45 | .laydate_body .laydate_chdown cite, .laydate_body .laydate_ym label{border-top-color:#666;} 46 | .laydate_body .laydate_chprev cite{border-right-style:solid; border-right-color:#666;} 47 | .laydate_body .laydate_chnext cite{border-left-style:solid; border-left-color:#666;} 48 | .laydate_body .laydate_table td{border:none; height:21px!important; line-height:21px!important; background-color:#fff;} 49 | .laydate_body .laydate_table .laydate_nothis{color:#999;} 50 | .laydate_body .laydate_table thead{height:21px!important; line-height:21px!important;} 51 | .laydate_body .laydate_table thead th{border-bottom:1px solid #ccc;} 52 | .laydate_body .laydate_bottom{border-bottom:1px solid #C6C6C6;} 53 | .laydate_body .laydate_bottom #laydate_hms{background-color:#fff;} 54 | .laydate_body .laydate_time{background-color:#fff;} 55 | .laydate_body .laydate_bottom .laydate_sj{border-right:1px solid #C6C6C6; background-color:#F6F6F6;} 56 | .laydate_body .laydate_bottom input{background-color:#fff;} 57 | .laydate_body .laydate_bottom .laydte_hsmtex{border-bottom:1px solid #C6C6C6;} 58 | .laydate_body .laydate_bottom .laydate_btn{border-right:1px solid #C6C6C6;} 59 | .laydate_body .laydate_bottom .laydate_v{color:#999} 60 | .laydate_body .laydate_bottom .laydate_btn a{border-right:none; background-color:#F6F6F6;} 61 | .laydate_body .laydate_bottom .laydate_btn a:hover{color:#000; background-color:#fff;} 62 | 63 | .laydate_body .laydate_m .laydate_yms span:hover, 64 | .laydate_body .laydate_y .laydate_yms ul li:hover, 65 | .laydate_body .laydate_table td:hover, 66 | .laydate_body .laydate_time .laydate_hmsno span:hover{background-color:#F3F3F3} 67 | 68 | 69 | -------------------------------------------------------------------------------- /webroot/js/skins/molv/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laynefyc/xhgui-branch/6ac92d46b04cec3cc728ab1a60818da94b4b1310/webroot/js/skins/molv/icon.png -------------------------------------------------------------------------------- /webroot/js/skins/molv/laydate.css: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @Name: laydate皮肤:墨绿 4 | @Author:贤心 5 | @Site:http://sentsin.com/layui/laydate 6 | 7 | **/ 8 | 9 | .laydate-icon{border:1px solid #ccc; background-image:url(icon.png)} 10 | 11 | .laydate_body .laydate_bottom #laydate_hms, 12 | .laydate_body .laydate_time{border:1px solid #ccc;} 13 | 14 | .laydate_body .laydate_box, 15 | .laydate_body .laydate_ym .laydate_yms, 16 | .laydate_body .laydate_time{box-shadow: 2px 2px 5px rgba(0,0,0,.1);} 17 | 18 | .laydate_body .laydate_box{border-top:none; border-bottom:none; background-color:#fff; color:#00625A;} 19 | .laydate_body .laydate_box input{background:none!important; color:#fff;} 20 | .laydate_body .laydate_box .laydate_void{color:#00E8D7!important;} 21 | .laydate_body .laydate_box a, .laydate_body .laydate_box a:hover{color:#00625A;} 22 | .laydate_body .laydate_box a:hover{color:#666;} 23 | .laydate_body .laydate_click{background-color:#009F95!important; color:#fff!important;} 24 | .laydate_body .laydate_top{border-top:1px solid #009F95; background-color:#009F95} 25 | .laydate_body .laydate_ym{border:1px solid #009F95; background-color:#009F95;} 26 | .laydate_body .laydate_ym .laydate_yms{border:1px solid #009F95; background-color:#009F95; color:#fff;} 27 | .laydate_body .laydate_y .laydate_yms a{border-bottom:1px solid #009F95;} 28 | .laydate_body .laydate_y .laydate_yms .laydate_chdown{border-top:1px solid #009F95; border-bottom:none;} 29 | .laydate_body .laydate_choose{border-left:1px solid #009F95;} 30 | .laydate_body .laydate_chprev{border-left:none; border-right:1px solid #009F95;} 31 | .laydate_body .laydate_choose:hover, 32 | .laydate_body .laydate_y .laydate_yms a:hover{background-color:#00C1B3;} 33 | .laydate_body .laydate_chtop cite{border-bottom-color:#fff;} 34 | .laydate_body .laydate_chdown cite, .laydate_body .laydate_ym label{border-top-color:#fff;} 35 | .laydate_body .laydate_chprev cite{border-right-style:solid; border-right-color:#fff;} 36 | .laydate_body .laydate_chnext cite{border-left-style:solid; border-left-color:#fff;} 37 | .laydate_body .laydate_table{width: 240px!important; margin: 0!important; border:1px solid #ccc; border-top:none; border-bottom:none;} 38 | .laydate_body .laydate_table td{border:none; height:21px!important; line-height:21px!important; background-color:#fff; color:#00625A;} 39 | .laydate_body .laydate_table .laydate_nothis{color:#999;} 40 | .laydate_body .laydate_table thead{border-bottom:1px solid #ccc; height:21px!important; line-height:21px!important;} 41 | .laydate_body .laydate_table thead th{} 42 | .laydate_body .laydate_bottom{border:1px solid #ccc; border-top:none;} 43 | .laydate_body .laydate_bottom #laydate_hms{background-color:#fff;} 44 | .laydate_body .laydate_time{background-color:#fff;} 45 | .laydate_body .laydate_time1{width: 226px!important; height: 152px!important;} 46 | .laydate_body .laydate_bottom .laydate_sj{width:31px!important; border-right:1px solid #ccc; background-color:#fff;} 47 | .laydate_body .laydate_bottom input{background-color:#fff; color:#00625A;} 48 | .laydate_body .laydate_bottom .laydte_hsmtex{border-bottom:1px solid #ccc;} 49 | .laydate_body .laydate_bottom .laydate_btn{border-right:1px solid #ccc;} 50 | .laydate_body .laydate_bottom .laydate_v{color:#999} 51 | .laydate_body .laydate_bottom .laydate_btn a{border: 1px solid #ccc; border-right:none; background-color:#fff;} 52 | .laydate_body .laydate_bottom .laydate_btn a:hover{background-color:#F6F6F6; color:#00625A;} 53 | 54 | .laydate_body .laydate_m .laydate_yms span:hover, 55 | .laydate_body .laydate_time .laydate_hmsno span:hover, 56 | .laydate_body .laydate_y .laydate_yms ul li:hover, 57 | .laydate_body .laydate_table td:hover{background-color:#00C1B3; color:#fff;} 58 | 59 | 60 | -------------------------------------------------------------------------------- /webroot/js/waterfall.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Render a waterfall chart into el using the data from url 3 | */ 4 | Xhgui.waterfall = function (el, options) { 5 | el = d3.select(el); 6 | 7 | // Use the containing element to get the width. 8 | var w = parseInt(el.style('width'), 10); 9 | 10 | d3.json(options.dataUrl, function (data) { 11 | var h = 50 + (30 * data.length), 12 | endTimes = [], 13 | startTimes = []; 14 | 15 | data.forEach(function (d) { 16 | d.startdt = new Date(d.start); 17 | d.enddt = new Date(d.start + d.duration); 18 | 19 | endTimes.push(d.enddt); 20 | startTimes.push(d.startdt); 21 | }); 22 | 23 | // Sort the set so it looks like a waterfall. 24 | data.sort(function (a, b) { 25 | if (a.start < b.start) { 26 | return -1; 27 | } 28 | if (a.start > b.start) { 29 | return 1; 30 | } 31 | return 0; 32 | }); 33 | 34 | 35 | var x = d3.time.scale().rangeRound([0, w]).nice(d3.time.second), 36 | y = d3.scale.linear().range([0, h]), 37 | xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(true), 38 | yAxis = d3.svg.axis().scale(y).ticks(4).orient("bottom"); 39 | 40 | var max = d3.max(endTimes); 41 | var min = d3.min(startTimes); 42 | 43 | var seconds = max.getSeconds(); 44 | max.setSeconds(seconds + 1); 45 | 46 | x.domain([min, max]); 47 | y.domain([0, data.length]); 48 | 49 | var svg = el.append('svg') 50 | .attr("width", w) 51 | .attr("height", h); 52 | 53 | svg.append("g") 54 | .attr("class", "x axis") 55 | .attr("transform", "translate(0," + (h - 20) + ")") 56 | .call(xAxis); 57 | 58 | var g = svg.selectAll('g.bar') 59 | .data(data).enter().append('g') 60 | .attr('class', 'bar') 61 | .attr('transform', function (d,i) { 62 | return 'translate(' + x(d.startdt) + ',' + y(i) + ')' 63 | }); 64 | 65 | g.append('rect') 66 | .attr('width', function (d) { 67 | var width = x(new Date(data[0].start + d.duration)); 68 | return width > 2 ? width : 3; 69 | }) 70 | .attr('height', 20); 71 | 72 | g.append('text').text(function (d, i) {return d.title; }) 73 | .attr('dy', '1em') 74 | .attr('fill','black') 75 | .attr("text-anchor", "left"); 76 | 77 | 78 | // Set tooltips on circles. 79 | Xhgui.tooltip(el, { 80 | bindTo: g, 81 | positioner: function (d, i) { 82 | // Use the translate attribute to position the tooltip. 83 | var transform = this.getAttribute('transform'); 84 | var position = this.getBBox(); 85 | 86 | var match = /translate\(([^,]+),([^\)]+)\)/.exec(transform); 87 | 88 | return { 89 | // 7 = 1/2 width of arrow 90 | x: parseFloat(match[1]) + (position.width / 2) - 7, 91 | // 25 = fudge factor. 92 | y: parseFloat(match[2]) - 25 93 | }; 94 | }, 95 | formatter: function (d, i) { 96 | var urlName = '?id=' + encodeURIComponent(d.id); 97 | var label = '' + decodeURIComponent(d.title) + '' + 98 | ' 详情
' + 99 | ' 耗时 ' + Xhgui.formatNumber(d.duration) + ' ms '; 100 | return label; 101 | } 102 | }); 103 | }); 104 | }; 105 | -------------------------------------------------------------------------------- /webroot/js/xhgui.js: -------------------------------------------------------------------------------- 1 | window.Xhgui = window.Xhgui || {}; 2 | 3 | Xhgui.tableSort = function(tables) { 4 | tables.stickyTableHeaders(); 5 | tables.tablesorter({ 6 | textExtraction: function(node) { 7 | if (node.className.match(/text/)) { 8 | return node.innerText; 9 | } 10 | var text = node.innerText || node.textContent; 11 | return '' + parseInt(text.replace(/,/g, ''), 10); 12 | } 13 | }); 14 | }; 15 | 16 | // Utilitarian DOM behavior. 17 | $(document).ready(function () { 18 | $('.tip').tooltip(); 19 | 20 | var tables = $('.table-sort'); 21 | Xhgui.tableSort(tables); 22 | 23 | 24 | // Bind events for expandable search forms. 25 | var searchForm = $('.search-form'), 26 | searchExpand = $('.search-expand'); 27 | 28 | searchExpand.on('click', function () { 29 | searchExpand.fadeOut('fast', function () { 30 | searchForm.slideDown('fast'); 31 | }); 32 | return false; 33 | }); 34 | 35 | $('.search-collapse').on('click', function () { 36 | searchForm.slideUp('fast', function () { 37 | searchExpand.show(); 38 | }); 39 | return false; 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /webroot/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------