├── .gitignore ├── LICENSE ├── README.md ├── docs ├── mac.jpg └── screen.png ├── metrics.metal ├── metrics.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata └── metrics ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json ├── DoubleLayerTimeSeriesView.h └── DoubleLayerTimeSeriesView.m ├── Base.lproj └── MainMenu.xib ├── DisplayLinkTimeSeriesView.h ├── DisplayLinkTimeSeriesView.m ├── DrawRectTimeSeriesView.h ├── DrawRectTimeSeriesView.m ├── DummyView.h ├── DummyView.m ├── Info.plist ├── MetalTimeSeriesView.h ├── MetalTimeSeriesView.m ├── MultiLayerTimeSeriesView.h ├── MultiLayerTimeSeriesView.m ├── RATilingBackgroundView.h ├── RATilingBackgroundView.m ├── RATilingBackgroundViewDelegate.h └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | 55 | #Code Injection 56 | # 57 | # After new code Injection tools there's a generated folder /iOSInjectionProject 58 | # https://github.com/johnno1962/injectionforxcode 59 | 60 | iOSInjectionProject/ 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Evadne Wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metal Metrics 2 | 3 | Draw as many horizon charts as you want with the power of a Macintosh (accelerated by [Metal](https://developer.apple.com/metal/)). 4 | 5 | ## Motive 6 | 7 | ### Motive 1: High Density, High Granularity 8 | 9 | In a modern data centre setting you may have more than a dozen of servers. Even if you have only a handful of servers, you may have many metrics per server. As the number of servers grow, the possible time series to monitor rapidly grows with it, and as we currently stand you are most likely restrained not by the amount of computing energy available, but by the extent of available visual real estate: every metric is to require a certain number of pixels to make sense. 10 | 11 | This means as the number of metrics you wish to monitor goes up, you either need to start coalescing time series to save space, or you need to cut down on granularity so as to fit more trend data in the same amount of space. 12 | 13 | Horizon charts to the rescue! 14 | 15 | A horizon chart is as compact as a sparkline, but conveys roughly the same amount of information easily in 25% of space that takes a line chart to do. This means you can fit 4x as much information in the same space without sacrificing granularity. In practice, sometimes peaks are “coalesced away” meaning that as the granularity gets coarser, interesting information disappears. You end up with nothing significant showing at the level of granularity, yet you continue to have a problem at hand, and need to try deliberately zooming in on every chart in order to find the problem. (NB: a biased sampling function may help, but I am not sure how much of a change it will bring you.) Obviously, granularity is never enough. 16 | 17 | ### Motive 2: High Frequency, High Performance 18 | 19 | The other problem that comes after having a high density visualisation is that you still want the computer running these charts to be cool enough to be useful doing something else. This means the charts need to be very performant. 20 | 21 | So I figured that if you can fit an entire 4K monitor with a load of these charts and have them all update at 30–60 FPS, then whatever your use case is, assuming that your tick interval is closer to one new data point per 5 seconds or per 1 minute, any implementation meeting the more stringent performance criterion will be able to meet your needs. 22 | 23 | So I set out to make a big window full of charts that are updated in real-time. 24 | 25 | ## Overview 26 | 27 | Simulated data source (one floating number generated per tick), one data source per chart, using colours from [Color Brewer](http://colorbrewer2.org) and in general implementing the Horizon Chart inspired by [Cubism](https://github.com/square/cubism). 28 | 29 | A few non-Metal approaches were also tried which clearly explains why, if your computer is connected to mains power, a Metal-driven solution is clearly the most efficient: 30 | 31 | * `DrawRectTimeSeriesView` — All chart instances share a single `NSTimer` which ticks six times per second. Chart refreshed in `-[NSView drawRect:]`, entire view redrawn since you are not to manipulate the bitmap context backing up NSView. 32 | 33 | * `DisplayLinkTimeSeriesView` — All chart instances share a single `CVDisplayLinkRef` which ticks once per frame. Elegant frame dropping implemented via Grand Central Dispatch, using Dispatch Groups. On each tick, backing image is manipulated (shifted one point to the left) with vImage in Accelerate and a new data point (in reality, a new slice) plotted with CGImage. 34 | 35 | * `DoubleLayerTimeSeriesView` — Incomplete implementation where 2 instances of the same CALayer holding reference to the same is shifted by one point on each tick so new values can be drawn in the middle of a fixed-size bitmap without having to push/invalidate all pixels around. Did not work out so well. 36 | 37 | Obviously they are all inferior to the Metal approach: 38 | 39 | * `MetalTimeSeriesView` — All chart instances sahre a single `CVDisplayLinkRef` which ticks once per frame. Rendition status controlled via GCD and a stupid static boolean variable (i.e. if there is capacity, then refresh all charts on the next tick, and do not refresh anything else until everything has refreshed once. In practice, this allows your charts to run at a lower frame rate if desired without locking up other bits of your interface). All chart instances backed by Metal and share the same Metal Command Queue. Vertex Buffer held as an instance variable and all vertices recomputed on each frame. (In the future they should get shifted.) Colours applied at the same time. (In the future this could be applied in the Metal program instead.) 40 | 41 | Also included: 42 | 43 | * `RATilingBackgroundView` — A very simple tiling view I have written ages ago (available from GitHub too), which fills a larger NSView with many small views. This is useful when stress-testing the implementation: you can fill an entire screen with many charts, and look for any kind of performance degradation or timing error. 44 | 45 | ## Pictures 46 | 47 | ![Rendered on Mac](docs/screen.png) 48 | 49 | ![Rendered on Mac](docs/mac.jpg) 50 | -------------------------------------------------------------------------------- /docs/mac.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evadne/metrics/2ff358c98bc50c9b0f0643f6b2e7f51a7f6fdfec/docs/mac.jpg -------------------------------------------------------------------------------- /docs/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evadne/metrics/2ff358c98bc50c9b0f0643f6b2e7f51a7f6fdfec/docs/screen.png -------------------------------------------------------------------------------- /metrics.metal: -------------------------------------------------------------------------------- 1 | // 2 | // metrics.metal 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 02/01/2017. 6 | // Copyright © 2017 Radius Development. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexInput { 13 | float2 position; 14 | short3 color; 15 | }; 16 | 17 | struct VertexOutput { 18 | float4 position [[position]]; 19 | short3 color; 20 | }; 21 | 22 | vertex VertexOutput vertex_main(device VertexInput *vertices [[buffer(0)]], uint vid [[vertex_id]]) { 23 | VertexInput vertexInput = vertices[vid]; 24 | 25 | return (VertexOutput){ 26 | (float4){ 27 | vertexInput.position[0], 28 | vertexInput.position[1], 29 | 0, 30 | 1 31 | }, 32 | vertexInput.color 33 | }; 34 | } 35 | 36 | fragment float4 fragment_main(VertexOutput inVertex [[stage_in]]) { 37 | return (float4) { 38 | (float)inVertex.color[0]/255.0f, 39 | (float)inVertex.color[1]/255.0f, 40 | (float)inVertex.color[2]/255.0f, 41 | 1 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /metrics.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FF0161E91E19F81700600A85 /* metrics.metal in Sources */ = {isa = PBXBuildFile; fileRef = FF0161E81E19F81600600A85 /* metrics.metal */; }; 11 | FF286E771E17475C00A63F10 /* DisplayLinkTimeSeriesView.m in Sources */ = {isa = PBXBuildFile; fileRef = FF286E761E17475C00A63F10 /* DisplayLinkTimeSeriesView.m */; }; 12 | FF403B581E0B07C100A58767 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = FF403B571E0B07C100A58767 /* AppDelegate.m */; }; 13 | FF403B5B1E0B07C100A58767 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = FF403B5A1E0B07C100A58767 /* main.m */; }; 14 | FF403B5D1E0B07C100A58767 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FF403B5C1E0B07C100A58767 /* Assets.xcassets */; }; 15 | FF403B601E0B07C100A58767 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = FF403B5E1E0B07C100A58767 /* MainMenu.xib */; }; 16 | FF403B691E0B07E400A58767 /* DrawRectTimeSeriesView.m in Sources */ = {isa = PBXBuildFile; fileRef = FF403B681E0B07E400A58767 /* DrawRectTimeSeriesView.m */; }; 17 | FF403B6C1E0B19FA00A58767 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF403B6B1E0B19FA00A58767 /* AppKit.framework */; }; 18 | FF403B711E0B4B4200A58767 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF403B6F1E0B1A4000A58767 /* CoreVideo.framework */; }; 19 | FF9A97861E142FEC004F9E25 /* RATilingBackgroundView.m in Sources */ = {isa = PBXBuildFile; fileRef = FF9A97841E142FEC004F9E25 /* RATilingBackgroundView.m */; }; 20 | FF9A97891E143C7E004F9E25 /* DummyView.m in Sources */ = {isa = PBXBuildFile; fileRef = FF9A97881E143C7E004F9E25 /* DummyView.m */; }; 21 | FF9EC12E1E17696B00908F23 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF9EC12D1E17696B00908F23 /* Accelerate.framework */; }; 22 | FF9EC12F1E1779A700908F23 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF403B6D1E0B1A0200A58767 /* QuartzCore.framework */; }; 23 | FF9EC1321E17925C00908F23 /* DoubleLayerTimeSeriesView.m in Sources */ = {isa = PBXBuildFile; fileRef = FF9EC1311E17925C00908F23 /* DoubleLayerTimeSeriesView.m */; }; 24 | FFA339FA1E19AD920023C720 /* MetalTimeSeriesView.m in Sources */ = {isa = PBXBuildFile; fileRef = FFA339F91E19AD920023C720 /* MetalTimeSeriesView.m */; }; 25 | FFADEFBE1E1995A000AADD47 /* MultiLayerTimeSeriesView.m in Sources */ = {isa = PBXBuildFile; fileRef = FFADEFBD1E1995A000AADD47 /* MultiLayerTimeSeriesView.m */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | FF0161E81E19F81600600A85 /* metrics.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = metrics.metal; sourceTree = ""; }; 30 | FF286E751E17475C00A63F10 /* DisplayLinkTimeSeriesView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DisplayLinkTimeSeriesView.h; sourceTree = ""; }; 31 | FF286E761E17475C00A63F10 /* DisplayLinkTimeSeriesView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DisplayLinkTimeSeriesView.m; sourceTree = ""; }; 32 | FF403B531E0B07C100A58767 /* metrics.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = metrics.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | FF403B561E0B07C100A58767 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 34 | FF403B571E0B07C100A58767 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 35 | FF403B5A1E0B07C100A58767 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 36 | FF403B5C1E0B07C100A58767 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | FF403B5F1E0B07C100A58767 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 38 | FF403B611E0B07C100A58767 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | FF403B671E0B07E400A58767 /* DrawRectTimeSeriesView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawRectTimeSeriesView.h; sourceTree = ""; }; 40 | FF403B681E0B07E400A58767 /* DrawRectTimeSeriesView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawRectTimeSeriesView.m; sourceTree = ""; }; 41 | FF403B6B1E0B19FA00A58767 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 42 | FF403B6D1E0B1A0200A58767 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 43 | FF403B6F1E0B1A4000A58767 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; 44 | FF9A97831E142FEC004F9E25 /* RATilingBackgroundView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RATilingBackgroundView.h; sourceTree = ""; }; 45 | FF9A97841E142FEC004F9E25 /* RATilingBackgroundView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RATilingBackgroundView.m; sourceTree = ""; }; 46 | FF9A97851E142FEC004F9E25 /* RATilingBackgroundViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RATilingBackgroundViewDelegate.h; sourceTree = ""; }; 47 | FF9A97871E143C7E004F9E25 /* DummyView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DummyView.h; sourceTree = ""; }; 48 | FF9A97881E143C7E004F9E25 /* DummyView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DummyView.m; sourceTree = ""; }; 49 | FF9EC12D1E17696B00908F23 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; 50 | FF9EC1301E17925C00908F23 /* DoubleLayerTimeSeriesView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DoubleLayerTimeSeriesView.h; path = Assets.xcassets/DoubleLayerTimeSeriesView.h; sourceTree = ""; }; 51 | FF9EC1311E17925C00908F23 /* DoubleLayerTimeSeriesView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DoubleLayerTimeSeriesView.m; path = Assets.xcassets/DoubleLayerTimeSeriesView.m; sourceTree = ""; }; 52 | FFA339F81E19AD920023C720 /* MetalTimeSeriesView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MetalTimeSeriesView.h; sourceTree = ""; }; 53 | FFA339F91E19AD920023C720 /* MetalTimeSeriesView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MetalTimeSeriesView.m; sourceTree = ""; }; 54 | FFADEFBC1E1995A000AADD47 /* MultiLayerTimeSeriesView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MultiLayerTimeSeriesView.h; sourceTree = ""; }; 55 | FFADEFBD1E1995A000AADD47 /* MultiLayerTimeSeriesView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MultiLayerTimeSeriesView.m; sourceTree = ""; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | FF403B501E0B07C100A58767 /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | FF9EC12F1E1779A700908F23 /* QuartzCore.framework in Frameworks */, 64 | FF9EC12E1E17696B00908F23 /* Accelerate.framework in Frameworks */, 65 | FF403B6C1E0B19FA00A58767 /* AppKit.framework in Frameworks */, 66 | FF403B711E0B4B4200A58767 /* CoreVideo.framework in Frameworks */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | /* End PBXFrameworksBuildPhase section */ 71 | 72 | /* Begin PBXGroup section */ 73 | FF403B4A1E0B07C100A58767 = { 74 | isa = PBXGroup; 75 | children = ( 76 | FF0161E81E19F81600600A85 /* metrics.metal */, 77 | FF403B551E0B07C100A58767 /* metrics */, 78 | FF403B541E0B07C100A58767 /* Products */, 79 | FF403B6A1E0B19FA00A58767 /* Frameworks */, 80 | ); 81 | sourceTree = ""; 82 | }; 83 | FF403B541E0B07C100A58767 /* Products */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | FF403B531E0B07C100A58767 /* metrics.app */, 87 | ); 88 | name = Products; 89 | sourceTree = ""; 90 | }; 91 | FF403B551E0B07C100A58767 /* metrics */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | FF403B561E0B07C100A58767 /* AppDelegate.h */, 95 | FF403B571E0B07C100A58767 /* AppDelegate.m */, 96 | FF9A97871E143C7E004F9E25 /* DummyView.h */, 97 | FF9A97881E143C7E004F9E25 /* DummyView.m */, 98 | FF403B671E0B07E400A58767 /* DrawRectTimeSeriesView.h */, 99 | FF403B681E0B07E400A58767 /* DrawRectTimeSeriesView.m */, 100 | FF286E751E17475C00A63F10 /* DisplayLinkTimeSeriesView.h */, 101 | FF286E761E17475C00A63F10 /* DisplayLinkTimeSeriesView.m */, 102 | FF9EC1301E17925C00908F23 /* DoubleLayerTimeSeriesView.h */, 103 | FF9EC1311E17925C00908F23 /* DoubleLayerTimeSeriesView.m */, 104 | FFADEFBC1E1995A000AADD47 /* MultiLayerTimeSeriesView.h */, 105 | FFADEFBD1E1995A000AADD47 /* MultiLayerTimeSeriesView.m */, 106 | FFA339F81E19AD920023C720 /* MetalTimeSeriesView.h */, 107 | FFA339F91E19AD920023C720 /* MetalTimeSeriesView.m */, 108 | FF9A97831E142FEC004F9E25 /* RATilingBackgroundView.h */, 109 | FF9A97841E142FEC004F9E25 /* RATilingBackgroundView.m */, 110 | FF9A97851E142FEC004F9E25 /* RATilingBackgroundViewDelegate.h */, 111 | FF403B5C1E0B07C100A58767 /* Assets.xcassets */, 112 | FF403B5E1E0B07C100A58767 /* MainMenu.xib */, 113 | FF403B611E0B07C100A58767 /* Info.plist */, 114 | FF403B591E0B07C100A58767 /* Supporting Files */, 115 | ); 116 | path = metrics; 117 | sourceTree = ""; 118 | }; 119 | FF403B591E0B07C100A58767 /* Supporting Files */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | FF403B5A1E0B07C100A58767 /* main.m */, 123 | ); 124 | name = "Supporting Files"; 125 | sourceTree = ""; 126 | }; 127 | FF403B6A1E0B19FA00A58767 /* Frameworks */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | FF9EC12D1E17696B00908F23 /* Accelerate.framework */, 131 | FF403B6F1E0B1A4000A58767 /* CoreVideo.framework */, 132 | FF403B6D1E0B1A0200A58767 /* QuartzCore.framework */, 133 | FF403B6B1E0B19FA00A58767 /* AppKit.framework */, 134 | ); 135 | name = Frameworks; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXNativeTarget section */ 141 | FF403B521E0B07C100A58767 /* metrics */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = FF403B641E0B07C100A58767 /* Build configuration list for PBXNativeTarget "metrics" */; 144 | buildPhases = ( 145 | FF403B4F1E0B07C100A58767 /* Sources */, 146 | FF403B501E0B07C100A58767 /* Frameworks */, 147 | FF403B511E0B07C100A58767 /* Resources */, 148 | ); 149 | buildRules = ( 150 | ); 151 | dependencies = ( 152 | ); 153 | name = metrics; 154 | productName = metrics; 155 | productReference = FF403B531E0B07C100A58767 /* metrics.app */; 156 | productType = "com.apple.product-type.application"; 157 | }; 158 | /* End PBXNativeTarget section */ 159 | 160 | /* Begin PBXProject section */ 161 | FF403B4B1E0B07C100A58767 /* Project object */ = { 162 | isa = PBXProject; 163 | attributes = { 164 | LastUpgradeCheck = 0820; 165 | ORGANIZATIONNAME = "Radius Development"; 166 | TargetAttributes = { 167 | FF403B521E0B07C100A58767 = { 168 | CreatedOnToolsVersion = 8.2; 169 | DevelopmentTeam = CDTGQDFDBU; 170 | ProvisioningStyle = Automatic; 171 | }; 172 | }; 173 | }; 174 | buildConfigurationList = FF403B4E1E0B07C100A58767 /* Build configuration list for PBXProject "metrics" */; 175 | compatibilityVersion = "Xcode 3.2"; 176 | developmentRegion = English; 177 | hasScannedForEncodings = 0; 178 | knownRegions = ( 179 | en, 180 | Base, 181 | ); 182 | mainGroup = FF403B4A1E0B07C100A58767; 183 | productRefGroup = FF403B541E0B07C100A58767 /* Products */; 184 | projectDirPath = ""; 185 | projectRoot = ""; 186 | targets = ( 187 | FF403B521E0B07C100A58767 /* metrics */, 188 | ); 189 | }; 190 | /* End PBXProject section */ 191 | 192 | /* Begin PBXResourcesBuildPhase section */ 193 | FF403B511E0B07C100A58767 /* Resources */ = { 194 | isa = PBXResourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | FF403B5D1E0B07C100A58767 /* Assets.xcassets in Resources */, 198 | FF403B601E0B07C100A58767 /* MainMenu.xib in Resources */, 199 | ); 200 | runOnlyForDeploymentPostprocessing = 0; 201 | }; 202 | /* End PBXResourcesBuildPhase section */ 203 | 204 | /* Begin PBXSourcesBuildPhase section */ 205 | FF403B4F1E0B07C100A58767 /* Sources */ = { 206 | isa = PBXSourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | FF9A97861E142FEC004F9E25 /* RATilingBackgroundView.m in Sources */, 210 | FF0161E91E19F81700600A85 /* metrics.metal in Sources */, 211 | FF403B5B1E0B07C100A58767 /* main.m in Sources */, 212 | FFADEFBE1E1995A000AADD47 /* MultiLayerTimeSeriesView.m in Sources */, 213 | FF403B691E0B07E400A58767 /* DrawRectTimeSeriesView.m in Sources */, 214 | FF286E771E17475C00A63F10 /* DisplayLinkTimeSeriesView.m in Sources */, 215 | FF9EC1321E17925C00908F23 /* DoubleLayerTimeSeriesView.m in Sources */, 216 | FFA339FA1E19AD920023C720 /* MetalTimeSeriesView.m in Sources */, 217 | FF403B581E0B07C100A58767 /* AppDelegate.m in Sources */, 218 | FF9A97891E143C7E004F9E25 /* DummyView.m in Sources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | /* End PBXSourcesBuildPhase section */ 223 | 224 | /* Begin PBXVariantGroup section */ 225 | FF403B5E1E0B07C100A58767 /* MainMenu.xib */ = { 226 | isa = PBXVariantGroup; 227 | children = ( 228 | FF403B5F1E0B07C100A58767 /* Base */, 229 | ); 230 | name = MainMenu.xib; 231 | sourceTree = ""; 232 | }; 233 | /* End PBXVariantGroup section */ 234 | 235 | /* Begin XCBuildConfiguration section */ 236 | FF403B621E0B07C100A58767 /* Debug */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 242 | CLANG_CXX_LIBRARY = "libc++"; 243 | CLANG_ENABLE_MODULES = YES; 244 | CLANG_ENABLE_OBJC_ARC = YES; 245 | CLANG_WARN_BOOL_CONVERSION = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 248 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 254 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 255 | CLANG_WARN_UNREACHABLE_CODE = YES; 256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 257 | CODE_SIGN_IDENTITY = "-"; 258 | COPY_PHASE_STRIP = NO; 259 | DEBUG_INFORMATION_FORMAT = dwarf; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | ENABLE_TESTABILITY = YES; 262 | GCC_C_LANGUAGE_STANDARD = gnu99; 263 | GCC_DYNAMIC_NO_PIC = NO; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_OPTIMIZATION_LEVEL = 0; 266 | GCC_PREPROCESSOR_DEFINITIONS = ( 267 | "DEBUG=1", 268 | "$(inherited)", 269 | ); 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | MACOSX_DEPLOYMENT_TARGET = 10.12; 277 | MTL_ENABLE_DEBUG_INFO = YES; 278 | MTL_FAST_MATH = YES; 279 | ONLY_ACTIVE_ARCH = YES; 280 | SDKROOT = macosx; 281 | }; 282 | name = Debug; 283 | }; 284 | FF403B631E0B07C100A58767 /* Release */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ALWAYS_SEARCH_USER_PATHS = NO; 288 | CLANG_ANALYZER_NONNULL = YES; 289 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 290 | CLANG_CXX_LIBRARY = "libc++"; 291 | CLANG_ENABLE_MODULES = YES; 292 | CLANG_ENABLE_OBJC_ARC = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_CONSTANT_CONVERSION = YES; 295 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 296 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 297 | CLANG_WARN_EMPTY_BODY = YES; 298 | CLANG_WARN_ENUM_CONVERSION = YES; 299 | CLANG_WARN_INFINITE_RECURSION = YES; 300 | CLANG_WARN_INT_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 303 | CLANG_WARN_UNREACHABLE_CODE = YES; 304 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 305 | CODE_SIGN_IDENTITY = "-"; 306 | COPY_PHASE_STRIP = NO; 307 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 308 | ENABLE_NS_ASSERTIONS = NO; 309 | ENABLE_STRICT_OBJC_MSGSEND = YES; 310 | GCC_C_LANGUAGE_STANDARD = gnu99; 311 | GCC_NO_COMMON_BLOCKS = YES; 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | MACOSX_DEPLOYMENT_TARGET = 10.12; 319 | MTL_ENABLE_DEBUG_INFO = NO; 320 | MTL_FAST_MATH = YES; 321 | SDKROOT = macosx; 322 | }; 323 | name = Release; 324 | }; 325 | FF403B651E0B07C100A58767 /* Debug */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 329 | CODE_SIGN_IDENTITY = "Mac Developer"; 330 | COMBINE_HIDPI_IMAGES = YES; 331 | DEVELOPMENT_TEAM = CDTGQDFDBU; 332 | INFOPLIST_FILE = metrics/Info.plist; 333 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 334 | MACOSX_DEPLOYMENT_TARGET = 10.12; 335 | PRODUCT_BUNDLE_IDENTIFIER = com.radius.metrics; 336 | PRODUCT_NAME = "$(TARGET_NAME)"; 337 | PROVISIONING_PROFILE_SPECIFIER = ""; 338 | }; 339 | name = Debug; 340 | }; 341 | FF403B661E0B07C100A58767 /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 345 | CODE_SIGN_IDENTITY = "Mac Developer"; 346 | COMBINE_HIDPI_IMAGES = YES; 347 | DEVELOPMENT_TEAM = CDTGQDFDBU; 348 | INFOPLIST_FILE = metrics/Info.plist; 349 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 350 | MACOSX_DEPLOYMENT_TARGET = 10.12; 351 | PRODUCT_BUNDLE_IDENTIFIER = com.radius.metrics; 352 | PRODUCT_NAME = "$(TARGET_NAME)"; 353 | PROVISIONING_PROFILE_SPECIFIER = ""; 354 | }; 355 | name = Release; 356 | }; 357 | /* End XCBuildConfiguration section */ 358 | 359 | /* Begin XCConfigurationList section */ 360 | FF403B4E1E0B07C100A58767 /* Build configuration list for PBXProject "metrics" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | FF403B621E0B07C100A58767 /* Debug */, 364 | FF403B631E0B07C100A58767 /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | FF403B641E0B07C100A58767 /* Build configuration list for PBXNativeTarget "metrics" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | FF403B651E0B07C100A58767 /* Debug */, 373 | FF403B661E0B07C100A58767 /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | /* End XCConfigurationList section */ 379 | }; 380 | rootObject = FF403B4B1E0B07C100A58767 /* Project object */; 381 | } 382 | -------------------------------------------------------------------------------- /metrics.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /metrics/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 21/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : NSObject 12 | 13 | @end 14 | 15 | 16 | -------------------------------------------------------------------------------- /metrics/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 21/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "DrawRectTimeSeriesView.h" 11 | #import "DoubleLayerTimeSeriesView.h" 12 | #import "DisplayLinkTimeSeriesView.h" 13 | #import "MetalTimeSeriesView.h" 14 | #import "MultiLayerTimeSeriesView.h" 15 | #import "DummyView.h" 16 | #import "RATilingBackgroundView.h" 17 | 18 | @interface AppDelegate () 19 | @property (weak) IBOutlet NSWindow *window; 20 | @end 21 | 22 | @implementation AppDelegate 23 | 24 | - (CGSize) sizeForTilesInTilingBackgroundView:(RATilingBackgroundView *)tilingBackgroundView { 25 | return (CGSize) { 128, 30 }; 26 | } 27 | 28 | - (NSView *) newTileForTilingBackgroundView:(RATilingBackgroundView *)tilingBackgroundView { 29 | #if 1 30 | return [[MetalTimeSeriesView alloc] initWithFrame:(CGRect){ 0, 0, 128, 30 }]; 31 | #endif 32 | 33 | #if 0 34 | return [[MultiLayerTimeSeriesView alloc] initWithFrame:(CGRect){ 0, 0, 120, 30 }]; 35 | #endif 36 | 37 | #if 0 38 | return [[DoubleLayerTimeSeriesView alloc] initWithFrame:(CGRect){ 0, 0, 120, 30 }]; 39 | #endif 40 | 41 | #if 0 42 | return [[DisplayLinkTimeSeriesView alloc] initWithFrame:(CGRect){ 0, 0, 128, 32 }]; 43 | #endif 44 | 45 | #if 0 46 | return [[DrawRectTimeSeriesView alloc] initWithFrame:(CGRect){ 0, 0, 128, 32 }]; 47 | #endif 48 | 49 | #if 0 50 | DummyView *newView = [[DummyView alloc] initWithFrame:(CGRect){ 0, 0, 128, 32 }]; 51 | CGFloat red = ((float_t)rand() / (float_t)RAND_MAX); 52 | CGFloat green = ((float_t)rand() / (float_t)RAND_MAX); 53 | CGFloat blue = ((float_t)rand() / (float_t)RAND_MAX); 54 | newView.backgroundColor = [NSColor colorWithSRGBRed:red green:green blue:blue alpha:1.0f]; 55 | return newView; 56 | #endif 57 | 58 | #if 0 59 | DummyView *newView = [[DummyView alloc] initWithFrame:(CGRect){ 0, 0, 128, 32 }]; 60 | newView.wantsLayer = YES; 61 | CGFloat red = ((float_t)rand() / (float_t)RAND_MAX); 62 | CGFloat green = ((float_t)rand() / (float_t)RAND_MAX); 63 | CGFloat blue = ((float_t)rand() / (float_t)RAND_MAX); 64 | newView.layer.backgroundColor = [NSColor colorWithSRGBRed:red green:green blue:blue alpha:1.0f].CGColor; 65 | return newView; 66 | #endif 67 | } 68 | 69 | @end 70 | -------------------------------------------------------------------------------- /metrics/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /metrics/Assets.xcassets/DoubleLayerTimeSeriesView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleLayerTimeSeriesView.h 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 31/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DoubleLayerTimeSeriesView : NSView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /metrics/Assets.xcassets/DoubleLayerTimeSeriesView.m: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleLayerTimeSeriesView.m 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 31/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "DoubleLayerTimeSeriesView.h" 12 | 13 | @interface DoubleLayerTimeSeriesView () 14 | @property (nonatomic, readonly, strong) CALayer *leftLayer; 15 | @property (nonatomic, readonly, strong) CALayer *rightLayer; 16 | @end 17 | 18 | @implementation DoubleLayerTimeSeriesView 19 | 20 | + (CGImageRef) newImageWithBackgroundColor:(NSColor *)color { 21 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 22 | uint32_t bitmapInfo = kCGImageAlphaNoneSkipFirst|kCGBitmapByteOrderDefault; 23 | size_t bufferWidth = 120; 24 | size_t bufferHeight = 30; 25 | CGContextRef context = CGBitmapContextCreate(NULL, bufferWidth, bufferHeight, 8, bufferWidth * 8, colorSpace, bitmapInfo); 26 | CGColorSpaceRelease(colorSpace); 27 | CGContextSetFillColorWithColor(context, color.CGColor); 28 | CGContextFillRect(context, (NSRect){ 0, 0, bufferWidth, bufferHeight }); 29 | CGImageRef image = CGBitmapContextCreateImage(context); 30 | CGContextRelease(context); 31 | return image; 32 | } 33 | 34 | - (id) initWithCoder:(NSCoder *)decoder { 35 | return [[super initWithCoder:decoder] commonInit]; 36 | } 37 | 38 | - (id) initWithFrame:(CGRect)frame { 39 | return [[super initWithFrame:frame] commonInit]; 40 | } 41 | 42 | - (id) commonInit { 43 | self.wantsLayer = YES; 44 | self.layer = [CALayer layer]; 45 | self.layer.backgroundColor = [NSColor whiteColor].CGColor; 46 | self.layer.opaque = YES; 47 | self.layer.masksToBounds = NO; 48 | self.layer.edgeAntialiasingMask = 0; 49 | 50 | _leftLayer = [CALayer layer]; 51 | _rightLayer = [CALayer layer]; 52 | 53 | _leftLayer.opaque = YES; 54 | _leftLayer.anchorPoint = CGPointZero; 55 | _leftLayer.contentsScale = 1.0f; 56 | _leftLayer.contentsGravity = kCAGravityTopLeft; 57 | _leftLayer.masksToBounds = NO; 58 | _leftLayer.edgeAntialiasingMask = 0; 59 | 60 | _rightLayer.opaque = YES; 61 | _rightLayer.anchorPoint = (CGPoint){ 1, 0 }; 62 | _rightLayer.contentsScale = 1.0f; 63 | _rightLayer.contentsGravity = kCAGravityTopLeft; 64 | _rightLayer.masksToBounds = NO; 65 | _rightLayer.edgeAntialiasingMask = 0; 66 | 67 | [self.layer addSublayer:_leftLayer]; 68 | [self.layer addSublayer:_rightLayer]; 69 | 70 | CGImageRef leftImage = [self.class newImageWithBackgroundColor:[NSColor yellowColor]]; 71 | _leftLayer.contents = (__bridge id)leftImage; 72 | CGImageRelease(leftImage); 73 | 74 | CGImageRef rightImage = [self.class newImageWithBackgroundColor:[NSColor purpleColor]]; 75 | _rightLayer.contents = (__bridge id)rightImage; 76 | CGImageRelease(rightImage); 77 | 78 | return self; 79 | } 80 | 81 | - (BOOL) isOpaque { 82 | return YES; 83 | } 84 | 85 | - (void) viewWillMoveToSuperview:(NSView *)newSuperview { 86 | [super viewWillMoveToSuperview:newSuperview]; 87 | if (newSuperview) { 88 | [self setNeedsLayout:YES]; 89 | } 90 | } 91 | 92 | - (void) layout { 93 | [CATransaction begin]; 94 | [CATransaction setDisableActions:YES]; 95 | 96 | CGRect bounds = self.layer.bounds; 97 | CGFloat stageWidth = CGRectGetWidth(bounds); 98 | CGFloat stageHeight = CGRectGetHeight(bounds); 99 | CGFloat speed = 30.0f; 100 | NSCParameterAssert(fmodf(stageWidth, speed) == 0.0f); 101 | NSCParameterAssert(fmodf(stageHeight, 1.0f) == 0.0f); 102 | 103 | [_leftLayer removeAllAnimations]; 104 | [_rightLayer removeAllAnimations]; 105 | 106 | CAMediaTimingFunction *timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 107 | 108 | CABasicAnimation *leftAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"]; 109 | leftAnimation.fromValue = [NSValue valueWithRect:(NSRect){ 0, 0, stageWidth, stageHeight }]; 110 | leftAnimation.toValue = [NSValue valueWithRect:(NSRect){ 0, 0, 0, stageHeight }]; 111 | leftAnimation.duration = stageWidth / speed; 112 | leftAnimation.repeatCount = MAXFLOAT; 113 | leftAnimation.timingFunction = timingFunction; 114 | 115 | CABasicAnimation *rightAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"]; 116 | rightAnimation.fromValue = [NSValue valueWithRect:(NSRect){ 0, 0, 0, stageHeight }]; 117 | rightAnimation.toValue = [NSValue valueWithRect:(NSRect){ 0, 0, stageWidth, stageHeight }]; 118 | rightAnimation.duration = stageWidth / speed; 119 | rightAnimation.repeatCount = MAXFLOAT; 120 | rightAnimation.timingFunction = timingFunction; 121 | 122 | [_leftLayer addAnimation:leftAnimation forKey:@"bounds"]; 123 | [_rightLayer addAnimation:rightAnimation forKey:@"bounds"]; 124 | 125 | _leftLayer.frame = bounds; 126 | _rightLayer.frame = bounds; 127 | 128 | #if 0 129 | CABasicAnimation *moveAnimation = [CABasicAnimation animationWithKeyPath:@"position.x"]; 130 | moveAnimation.fromValue = @(0.0f); 131 | moveAnimation.toValue = @(-1.0f * stageWidth); 132 | moveAnimation.duration = stageWidth / speed; 133 | moveAnimation.removedOnCompletion = YES; 134 | moveAnimation.repeatCount = MAXFLOAT; 135 | moveAnimation.additive = YES; 136 | 137 | _leftLayer.frame = bounds; 138 | _rightLayer.frame = CGRectOffset(bounds, stageWidth, 0.0f); 139 | [_leftLayer addAnimation:moveAnimation forKey:@"positioning"]; 140 | [_rightLayer addAnimation:moveAnimation forKey:@"positioning"]; 141 | #endif 142 | 143 | [CATransaction commit]; 144 | } 145 | 146 | @end 147 | -------------------------------------------------------------------------------- /metrics/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | Default 540 | 541 | 542 | 543 | 544 | 545 | 546 | Left to Right 547 | 548 | 549 | 550 | 551 | 552 | 553 | Right to Left 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | Default 565 | 566 | 567 | 568 | 569 | 570 | 571 | Left to Right 572 | 573 | 574 | 575 | 576 | 577 | 578 | Right to Left 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | -------------------------------------------------------------------------------- /metrics/DisplayLinkTimeSeriesView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayLinkTimeSeriesView.h 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 31/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DisplayLinkTimeSeriesView : NSView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /metrics/DisplayLinkTimeSeriesView.m: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayLinkTimeSeriesView.m 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 31/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | #import "DisplayLinkTimeSeriesView.h" 13 | 14 | @interface DisplayLinkTimeSeriesView () 15 | + (NSMutableSet *) timerTargets; 16 | - (void) tick:(const CVTimeStamp *)outputTime; 17 | + (NSArray *) colors; 18 | + (NSArray *) positiveColors; 19 | + (NSArray *) negativeColors; 20 | 21 | @property (nonatomic, readonly, assign) CGContextRef inContext; 22 | @property (nonatomic, readonly, assign) CGContextRef outContext; 23 | @property (nonatomic, readonly, assign) vImage_Buffer inBuffer; 24 | @property (nonatomic, readonly, assign) vImage_Buffer outBuffer; 25 | @property (nonatomic, readonly, assign) NSUInteger lastTimestamp; 26 | @property (nonatomic, readonly, strong) NSMutableArray *values; 27 | @end 28 | 29 | static CVReturn DisplayLinkTimeSeriesViewCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) { 30 | NSArray *targets = [DisplayLinkTimeSeriesView.timerTargets copy]; 31 | 32 | dispatch_group_t group = dispatch_group_create(); 33 | for (DisplayLinkTimeSeriesView *target in targets) { 34 | dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 35 | [target tick:outputTime]; 36 | }); 37 | } 38 | 39 | dispatch_group_wait(group, DISPATCH_TIME_FOREVER); 40 | 41 | dispatch_sync(dispatch_get_main_queue(), ^{ 42 | for (DisplayLinkTimeSeriesView *target in targets) { 43 | [target setNeedsDisplay:YES]; 44 | [target displayIfNeeded]; 45 | } 46 | }); 47 | 48 | return kCVReturnSuccess; 49 | } 50 | 51 | @implementation DisplayLinkTimeSeriesView 52 | 53 | - (id) initWithCoder:(NSCoder *)decoder { 54 | return [[super initWithCoder:decoder] commonInit]; 55 | } 56 | 57 | - (id) initWithFrame:(CGRect)frame { 58 | return [[super initWithFrame:frame] commonInit]; 59 | } 60 | 61 | - (id) commonInit { 62 | _values = [NSMutableArray array]; 63 | self.wantsLayer = YES; 64 | self.layer.backgroundColor = [NSColor whiteColor].CGColor; 65 | self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; 66 | 67 | __weak typeof(self) wSelf = self; 68 | 69 | [NSNotificationCenter.defaultCenter addObserverForName:NSViewFrameDidChangeNotification object:self queue:nil usingBlock:^(NSNotification * _Nonnull note) { 70 | [wSelf rebuildBuffers]; 71 | }]; 72 | 73 | [NSNotificationCenter.defaultCenter addObserverForName:NSViewBoundsDidChangeNotification object:self queue:nil usingBlock:^(NSNotification * _Nonnull note) { 74 | [wSelf rebuildBuffers]; 75 | }]; 76 | 77 | return self; 78 | } 79 | 80 | - (BOOL) wantsUpdateLayer { 81 | return YES; 82 | } 83 | 84 | - (BOOL) isOpaque { 85 | return YES; 86 | } 87 | 88 | - (void) updateLayer { 89 | if (_outContext) { 90 | CGImageRef image = CGBitmapContextCreateImage(_outContext); 91 | self.layer.contents = (__bridge id)image; 92 | CGImageRelease(image); 93 | } 94 | } 95 | 96 | - (void) dealloc { 97 | [NSNotificationCenter.defaultCenter removeObserver:self]; 98 | } 99 | 100 | - (void) viewWillMoveToSuperview:(NSView *)newSuperview { 101 | [super viewWillMoveToSuperview:newSuperview]; 102 | if (newSuperview) { 103 | self.postsFrameChangedNotifications = YES; 104 | self.postsBoundsChangedNotifications = YES; 105 | [self.class displayLink]; 106 | [self.class.timerTargets addObject:self]; 107 | } else { 108 | self.postsFrameChangedNotifications = NO; 109 | self.postsBoundsChangedNotifications = NO; 110 | [self.class.timerTargets removeObject:self]; 111 | } 112 | } 113 | 114 | + (NSMutableSet *) timerTargets { 115 | static NSMutableSet *timerTargets = nil; 116 | static dispatch_once_t onceToken; 117 | dispatch_once(&onceToken, ^{ 118 | timerTargets = [NSMutableSet set]; 119 | }); 120 | 121 | return timerTargets; 122 | } 123 | 124 | + (CVDisplayLinkRef) displayLink { 125 | static CVDisplayLinkRef displayLink = nil; 126 | static dispatch_once_t onceToken; 127 | 128 | dispatch_once(&onceToken, ^{ 129 | CVDisplayLinkRef displayLink = NULL; 130 | CVDisplayLinkCreateWithActiveCGDisplays(&displayLink); 131 | CVDisplayLinkSetOutputCallback(displayLink, &DisplayLinkTimeSeriesViewCallback, (__bridge void *)self); 132 | CVDisplayLinkStart(displayLink); 133 | }); 134 | 135 | return displayLink; 136 | } 137 | 138 | + (NSArray *) colors { 139 | static dispatch_once_t token; 140 | static NSArray *colors = nil; 141 | 142 | dispatch_once(&token, ^{ 143 | NSColor * (^colourFromComponents)(NSUInteger, NSUInteger, NSUInteger) = ^ (NSUInteger red, NSUInteger green, NSUInteger blue) { 144 | return [NSColor colorWithSRGBRed:(((float_t)red)/256.0f) green:(((float_t)green)/256.0f) blue:(((float_t)blue)/256.0f) alpha:1.0f]; 145 | }; 146 | 147 | colors = @[ 148 | colourFromComponents(5,112,176), 149 | colourFromComponents(116,169,207), 150 | colourFromComponents(189,201,225), 151 | colourFromComponents(241,238,246), 152 | colourFromComponents(237,248,251), 153 | colourFromComponents(178,226,226), 154 | colourFromComponents(102,194,164), 155 | colourFromComponents(35,139,69) 156 | ]; 157 | }); 158 | return colors; 159 | } 160 | 161 | + (NSArray *) positiveColors { 162 | static dispatch_once_t token; 163 | static NSArray *positiveColors = nil; 164 | 165 | dispatch_once(&token, ^{ 166 | NSColor * (^colourFromComponents)(NSUInteger, NSUInteger, NSUInteger) = ^ (NSUInteger red, NSUInteger green, NSUInteger blue) { 167 | return [NSColor colorWithSRGBRed:(((float_t)red)/256.0f) green:(((float_t)green)/256.0f) blue:(((float_t)blue)/256.0f) alpha:1.0f]; 168 | }; 169 | 170 | positiveColors = @[ 171 | colourFromComponents(237,248,251), 172 | colourFromComponents(178,226,226), 173 | colourFromComponents(102,194,164), 174 | colourFromComponents(35,139,69) 175 | ]; 176 | }); 177 | return positiveColors; 178 | } 179 | 180 | + (NSArray *) negativeColors { 181 | static dispatch_once_t token; 182 | static NSArray *negativeColors = nil; 183 | 184 | dispatch_once(&token, ^{ 185 | NSColor * (^colourFromComponents)(NSUInteger, NSUInteger, NSUInteger) = ^ (NSUInteger red, NSUInteger green, NSUInteger blue) { 186 | return [NSColor colorWithSRGBRed:(((float_t)red)/256.0f) green:(((float_t)green)/256.0f) blue:(((float_t)blue)/256.0f) alpha:1.0f]; 187 | }; 188 | 189 | negativeColors = @[ 190 | colourFromComponents(241,238,246), 191 | colourFromComponents(189,201,225), 192 | colourFromComponents(116,169,207), 193 | colourFromComponents(5,112,176) 194 | ]; 195 | }); 196 | return negativeColors; 197 | } 198 | 199 | - (void) fillValue { 200 | NSNumber *lastNumber; 201 | float_t target = ((lastNumber = self.values.lastObject)) ? 202 | (lastNumber.floatValue + (0.1f * ((float)rand()/(float)RAND_MAX) - 0.05f)) : 203 | (2.0f * ((float)rand()/(float)RAND_MAX) - 1.0f); 204 | [self.values addObject:@(MIN(1.0f, MAX(-1.0f, target)))]; 205 | 206 | if (self.values.count > 128) { 207 | [self.values removeObjectsInRange:(NSRange){ 0, self.values.count - 128 }]; 208 | } 209 | } 210 | 211 | - (void) rebuildBuffers { 212 | CGSize bufferSize = self.bounds.size; 213 | if (bufferSize.width == 0.0 || bufferSize.height == 0.0) { 214 | return; 215 | } 216 | 217 | size_t bufferWidth = (size_t)rint(bufferSize.width); 218 | if (bufferWidth == 0) { 219 | bufferWidth = 1; 220 | } 221 | 222 | size_t bufferHeight = (size_t)rint(bufferSize.height); 223 | if (bufferHeight == 0) { 224 | bufferHeight = 1; 225 | } 226 | 227 | if (_inContext) 228 | if (CGBitmapContextGetWidth(_inContext) == bufferWidth) 229 | if (CGBitmapContextGetHeight(_inContext) == bufferHeight) 230 | if (_outContext) 231 | if (CGBitmapContextGetWidth(_outContext) == bufferWidth) 232 | if (CGBitmapContextGetHeight(_outContext) == bufferHeight) 233 | return; 234 | 235 | if (_inContext) { 236 | CGContextRelease(_inContext); 237 | _inContext = NULL; 238 | } 239 | 240 | if (_outContext) { 241 | CGContextRelease(_outContext); 242 | _outContext = NULL; 243 | } 244 | 245 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 246 | uint32_t bitmapInfo = kCGImageAlphaPremultipliedFirst|kCGBitmapByteOrder32Big; 247 | 248 | _inContext = CGBitmapContextCreate(NULL, bufferWidth, bufferHeight, 8, bufferWidth * 8, colorSpace, bitmapInfo); 249 | _outContext = CGBitmapContextCreate(NULL, bufferWidth, bufferHeight, 8, bufferWidth * 8, colorSpace, bitmapInfo); 250 | CGColorSpaceRelease(colorSpace); 251 | 252 | _inBuffer = (vImage_Buffer){ 253 | .data = CGBitmapContextGetData(_inContext), 254 | .width = CGBitmapContextGetWidth(_inContext), 255 | .height = CGBitmapContextGetHeight(_inContext), 256 | .rowBytes = CGBitmapContextGetBytesPerRow(_inContext) 257 | }; 258 | 259 | _outBuffer = (vImage_Buffer){ 260 | .data = CGBitmapContextGetData(_outContext), 261 | .width = CGBitmapContextGetWidth(_outContext), 262 | .height = CGBitmapContextGetHeight(_outContext), 263 | .rowBytes = CGBitmapContextGetBytesPerRow(_outContext) 264 | }; 265 | } 266 | 267 | - (void) tick:(const CVTimeStamp *)outputTime { 268 | [self fillValue]; 269 | 270 | if (self.inLiveResize) { 271 | return; 272 | } 273 | 274 | if (!_inContext || !_outContext) { 275 | return; 276 | } 277 | 278 | NSUInteger fromTimestamp = self.lastTimestamp; 279 | NSUInteger toTimestamp = (outputTime->videoTime / outputTime->videoRefreshPeriod); 280 | NSUInteger maxVisibleSteps = (NSUInteger)ceilf(CGRectGetWidth(self.bounds) / self.stepWidth); 281 | NSUInteger availableSteps = self.values.count; 282 | NSUInteger wantedSteps = MIN(MIN(availableSteps, maxVisibleSteps), (toTimestamp - fromTimestamp)); 283 | 284 | vImage_Buffer tmpBuffer = _inBuffer; 285 | _inBuffer = _outBuffer; 286 | _outBuffer = tmpBuffer; 287 | 288 | CGContextRef tmpContext = _inContext; 289 | _inContext = _outContext; 290 | _outContext = tmpContext; 291 | 292 | vImage_CGAffineTransform transform = (vImage_CGAffineTransform){ 1, 0, 0, 1, -1.0f * (float_t)wantedSteps * self.stepWidth, 0.0f }; 293 | 294 | static uint8_t backColor[4] = {0}; 295 | static vImage_Flags flags = kvImageBackgroundColorFill; 296 | vImageAffineWarpCG_ARGB8888(&_inBuffer, &_outBuffer, NULL, &transform, backColor, flags); 297 | 298 | CGContextSetFillColorWithColor(_outContext, self.class.colors[toTimestamp % 8].CGColor); 299 | CGContextFillRect(_outContext, (CGRect){ self.bounds.size.width - (float_t)wantedSteps * self.stepWidth, 0.0f, self.stepWidth, self.bounds.size.height }); 300 | 301 | _lastTimestamp = toTimestamp; 302 | } 303 | 304 | - (CGFloat) stepWidth { 305 | return 1.0f; 306 | } 307 | 308 | @end 309 | -------------------------------------------------------------------------------- /metrics/DrawRectTimeSeriesView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DrawRectTimeSeriesView.h 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 21/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DrawRectTimeSeriesView : NSView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /metrics/DrawRectTimeSeriesView.m: -------------------------------------------------------------------------------- 1 | // 2 | // DrawRectTimeSeriesView.m 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 21/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import "DrawRectTimeSeriesView.h" 10 | #import 11 | 12 | NSColor * colourFromComponents (NSUInteger red, NSUInteger green, NSUInteger blue) { 13 | return [NSColor colorWithSRGBRed:(((float_t)red)/256.0f) green:(((float_t)green)/256.0f) blue:(((float_t)blue)/256.0f) alpha:1.0f]; 14 | } 15 | 16 | @interface DrawRectTimeSeriesView () 17 | @property (strong) NSArray *positiveColours; 18 | @property (strong) NSArray *negativeColours; 19 | @property (strong, nonatomic) NSMutableArray *values; 20 | @property (strong) NSTimer *timer; 21 | @end 22 | 23 | @implementation DrawRectTimeSeriesView 24 | 25 | + (NSMutableSet *) timerTargets { 26 | static NSMutableSet *timerTargets = nil; 27 | static dispatch_once_t onceToken; 28 | dispatch_once(&onceToken, ^{ 29 | timerTargets = [NSMutableSet set]; 30 | }); 31 | 32 | return timerTargets; 33 | } 34 | 35 | + (NSTimer *) timer { 36 | static NSTimer *timer = nil; 37 | static dispatch_once_t onceToken; 38 | 39 | dispatch_once(&onceToken, ^{ 40 | __weak typeof(self) wSelf = self; 41 | timer = [NSTimer scheduledTimerWithTimeInterval:(10.0f / 60.0f) repeats:YES block:^(NSTimer * _Nonnull timer) { 42 | for (void(^block)(void) in wSelf.timerTargets) { 43 | block(); 44 | } 45 | }]; 46 | }); 47 | 48 | return timer; 49 | } 50 | 51 | - (BOOL) isOpaque { 52 | return YES; 53 | } 54 | 55 | - (BOOL) wantsDefaultClipping { 56 | return NO; 57 | } 58 | 59 | - (id) initWithCoder:(NSCoder *)decoder { 60 | self = [super initWithCoder:decoder]; 61 | if (!self) { 62 | return nil; 63 | } 64 | 65 | [self commonInit]; 66 | return self; 67 | } 68 | 69 | - (id) initWithFrame:(CGRect)frame { 70 | self = [super initWithFrame:frame]; 71 | if (!self) { 72 | return nil; 73 | } 74 | 75 | [self commonInit]; 76 | return self; 77 | } 78 | 79 | - (void) commonInit { 80 | __weak typeof(self) wSelf = self; 81 | 82 | // [super awakeFromNib]; 83 | 84 | [self.class timer]; 85 | [self.class.timerTargets addObject:(id)[^{ 86 | [wSelf update]; 87 | } copy]]; 88 | 89 | self.positiveColours = @[ 90 | colourFromComponents(237,248,251), 91 | colourFromComponents(178,226,226), 92 | colourFromComponents(102,194,164), 93 | colourFromComponents(35,139,69) 94 | ]; 95 | self.negativeColours = @[ 96 | colourFromComponents(241,238,246), 97 | colourFromComponents(189,201,225), 98 | colourFromComponents(116,169,207), 99 | colourFromComponents(5,112,176) 100 | ]; 101 | } 102 | 103 | - (void) update { 104 | NSCParameterAssert(nil); 105 | [self fillValue]; 106 | [self setNeedsDisplay:YES]; 107 | } 108 | 109 | - (NSMutableArray *) values { 110 | if (!_values) { 111 | _values = [NSMutableArray array]; 112 | } 113 | return _values; 114 | } 115 | 116 | - (void) fillValue { 117 | NSNumber *lastNumber; 118 | float_t target = ((lastNumber = self.values.lastObject)) ? 119 | (lastNumber.floatValue + (0.1f * ((float)rand()/(float)RAND_MAX) - 0.05f)) : 120 | (2.0f * ((float)rand()/(float)RAND_MAX) - 1.0f); 121 | [self.values addObject:@(MIN(1.0f, MAX(-1.0f, target)))]; 122 | } 123 | 124 | - (CGFloat) stepWidth { 125 | return 64.0f; 126 | } 127 | 128 | - (void) drawRect:(NSRect)dirtyRect { 129 | CGContextRef context = NSGraphicsContext.currentContext.CGContext; 130 | [NSColor.whiteColor set]; 131 | CGContextFillRect(context, self.bounds); 132 | CGRect bounds = self.bounds; 133 | CGFloat boundsWidth = CGRectGetWidth(bounds); 134 | CGFloat boundsHeight = CGRectGetHeight(bounds); 135 | CGFloat stepWidth = self.stepWidth; 136 | NSUInteger availableNumberOfSteps = self.values.count; 137 | if (availableNumberOfSteps == 0) { 138 | return; 139 | } 140 | NSUInteger maximumNumberOfSteps = floorf(boundsWidth / stepWidth); 141 | NSUInteger fromStep = MAX(availableNumberOfSteps, maximumNumberOfSteps) - maximumNumberOfSteps; 142 | 143 | for (NSUInteger stepOffset = 0; stepOffset < (availableNumberOfSteps - fromStep); stepOffset++) { 144 | NSUInteger step = fromStep + stepOffset; 145 | CGFloat offsetX = (fromStep == 0) ? 146 | (step * stepWidth) : 147 | (boundsWidth - (availableNumberOfSteps - step) * stepWidth); 148 | float_t value = ((NSNumber *)self.values[step]).floatValue; 149 | float_t absValue = fabsf(value); 150 | if (value != 0.0f) { 151 | NSArray *colours = (value > 0) ? self.positiveColours : self.negativeColours; 152 | NSUInteger numberOfBands = colours.count; 153 | NSUInteger band = ceilf(absValue * (float_t)numberOfBands); 154 | CGFloat fraction = (absValue - (1.0f / numberOfBands) * (band - 1)); 155 | if (band > 1) { 156 | [(NSColor *)colours[band - 2] set]; 157 | CGContextFillRect(context, (CGRect){ offsetX, boundsHeight * fraction, stepWidth, boundsHeight * (1.0f - fraction) }); 158 | } 159 | [(NSColor *)colours[band - 1] set]; 160 | CGContextFillRect(context, (CGRect){ offsetX, 0, stepWidth, boundsHeight * fraction }); 161 | } 162 | } 163 | } 164 | 165 | @end 166 | -------------------------------------------------------------------------------- /metrics/DummyView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DummyView.h 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 28/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DummyView : NSView 12 | @property (nonatomic, readwrite, strong) NSColor *backgroundColor; 13 | @end 14 | -------------------------------------------------------------------------------- /metrics/DummyView.m: -------------------------------------------------------------------------------- 1 | // 2 | // DummyView.m 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 28/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import "DummyView.h" 10 | 11 | @implementation DummyView 12 | // 13 | //- (BOOL) wantsUpdateLayer { 14 | // return YES; 15 | //} 16 | // 17 | //- (BOOL) wantsLayer { 18 | // return YES; 19 | //} 20 | // 21 | //- (void) updateLayer { 22 | // [super updateLayer]; 23 | //// self.layer.backgroundColor = self.backgroundColor.CGColor; 24 | //} 25 | 26 | - (BOOL) wantsDefaultClipping { 27 | return NO; 28 | } 29 | 30 | - (void)drawRect:(NSRect)dirtyRect { 31 | [super drawRect:dirtyRect]; 32 | CGColorRef color = _backgroundColor.CGColor; 33 | if (color) { 34 | CGContextRef context = NSGraphicsContext.currentContext.CGContext; 35 | CGContextSaveGState(context); 36 | CGContextSetFillColorWithColor(context, color); 37 | CGContextFillRect(context, dirtyRect); 38 | CGContextRestoreGState(context); 39 | } 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /metrics/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2016 Radius Development. All rights reserved. 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | NSSupportsAutomaticGraphicsSwitching 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /metrics/MetalTimeSeriesView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MetalTimeSeriesView.h 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 01/01/2017. 6 | // Copyright © 2017 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface MetalTimeSeriesView : MTKView 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /metrics/MetalTimeSeriesView.m: -------------------------------------------------------------------------------- 1 | // 2 | // MetalTimeSeriesView.m 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 01/01/2017. 6 | // Copyright © 2017 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "MetalTimeSeriesView.h" 11 | 12 | typedef struct { 13 | vector_float2 position; 14 | vector_short3 color; 15 | } MTSVertex; 16 | 17 | static vector_short3 positiveColors[4] = (vector_short3[]){ 18 | { 237, 248, 251 }, 19 | { 178, 226, 226 }, 20 | { 102, 194, 164 }, 21 | { 35, 139, 69 } 22 | }; 23 | 24 | static vector_short3 negativeColors[4] = (vector_short3[]){ 25 | { 241, 238, 246 }, 26 | { 189, 201, 225 }, 27 | { 116,169, 207 }, 28 | { 5, 112, 176 } 29 | }; 30 | 31 | NS_INLINE int bandNumberForValue (float_t value) { 32 | return (value >= .75f) ? 4 : 33 | (value >= .5f) ? 3 : 34 | (value >= .25f) ? 2 : 35 | (value > 0) ? 1 : 36 | (value == 0) ? 0 : 37 | (value > -.25f) ? -1 : 38 | (value > -.5f) ? -2 : 39 | (value > -.75f) ? -3 : 40 | -4; 41 | } 42 | 43 | NS_INLINE float_t nextValue (float_t value) { 44 | return MAX(-1.0f, MIN(1.0f, (value + (0.06f * ((float)rand()/(float)RAND_MAX) - 0.03f)))); 45 | } 46 | 47 | @interface MetalTimeSeriesView () 48 | + (NSMutableSet *) timerTargets; 49 | + (void) setupDisplayLink; 50 | - (void) tick:(const CVTimeStamp *)outputTime; 51 | @property (nonatomic, readonly, assign) BOOL hasRendered; 52 | @property (nonatomic, readonly, assign) float_t *values; 53 | @property (nonatomic, readonly, assign) size_t numberOfValues; 54 | @property (nonatomic, readonly, strong) id vertexBuffer; 55 | @property (nonatomic, readonly, strong) id renderPipeline; 56 | @end 57 | 58 | static CVReturn MetalTimeSeriesViewCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) { 59 | static BOOL isRendering = NO; 60 | if (isRendering) 61 | return kCVReturnRetry; 62 | 63 | // static CFTimeInterval lastRenderTime = 0; 64 | // CFTimeInterval nowRenderTime = CACurrentMediaTime(); 65 | // if ((lastRenderTime >= 0) && ((nowRenderTime - lastRenderTime) < 1.0f/60.0f)) // cap to 60FPS, 30FPS, 20FPS, … 66 | // return kCVReturnRetry; 67 | 68 | isRendering = YES; 69 | dispatch_async(dispatch_get_main_queue(), ^{ 70 | NSArray *targets = [MetalTimeSeriesView.timerTargets copy]; 71 | for (MetalTimeSeriesView *target in targets) { 72 | [target tick:outputTime]; 73 | [target setNeedsDisplay:YES]; 74 | } 75 | // lastRenderTime = nowRenderTime; 76 | isRendering = NO; 77 | }); 78 | 79 | return kCVReturnSuccess; 80 | } 81 | 82 | @implementation MetalTimeSeriesView 83 | 84 | + (NSMutableSet *) timerTargets { 85 | static NSMutableSet *timerTargets = nil; 86 | static dispatch_once_t onceToken; 87 | dispatch_once(&onceToken, ^{ 88 | timerTargets = [NSMutableSet set]; 89 | }); 90 | 91 | return timerTargets; 92 | } 93 | 94 | + (void) setupDisplayLink { 95 | static CVDisplayLinkRef displayLink = nil; 96 | static dispatch_once_t onceToken; 97 | dispatch_once(&onceToken, ^{ 98 | CVDisplayLinkCreateWithActiveCGDisplays(&displayLink); 99 | CVDisplayLinkSetOutputCallback(displayLink, &MetalTimeSeriesViewCallback, (__bridge void *)self); 100 | 101 | CVDisplayLinkStart(displayLink); 102 | 103 | for (NSString *notificationName in @[ 104 | NSWindowWillMoveNotification, 105 | NSWindowWillStartLiveResizeNotification, 106 | NSWindowWillEnterFullScreenNotification, 107 | NSWindowWillEnterVersionBrowserNotification 108 | ]) { 109 | [NSNotificationCenter.defaultCenter addObserverForName:notificationName object:nil queue:nil usingBlock:^(NSNotification *note) { CVDisplayLinkStop(displayLink); }]; 110 | }; 111 | 112 | for (NSString *notificationName in @[ 113 | NSWindowDidMoveNotification, 114 | NSWindowDidEndLiveResizeNotification, 115 | NSWindowDidExitFullScreenNotification, 116 | NSWindowDidExitVersionBrowserNotification 117 | ]) { 118 | [NSNotificationCenter.defaultCenter addObserverForName:notificationName object:nil queue:nil usingBlock:^(NSNotification *note) { CVDisplayLinkStart(displayLink); }]; 119 | }; 120 | 121 | [NSNotificationCenter.defaultCenter addObserverForName:NSWindowDidChangeOcclusionStateNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { 122 | BOOL hasVisibleTargets = NO; 123 | for (NSView *view in self.timerTargets) { 124 | if (view.window.occlusionState & NSWindowOcclusionStateVisible) { 125 | hasVisibleTargets = YES; 126 | break; 127 | } 128 | } 129 | 130 | if (hasVisibleTargets) { 131 | CVDisplayLinkStart(displayLink); 132 | } else { 133 | CVDisplayLinkStop(displayLink); 134 | } 135 | }]; 136 | }); 137 | } 138 | 139 | - (void) tick:(const CVTimeStamp *)outputTime { 140 | for (size_t i = 0; i < (_numberOfValues - 1); i++) { 141 | _values[i] = _values[i+1]; 142 | } 143 | _values[_numberOfValues - 1] = nextValue(_values[_numberOfValues - 2]); 144 | [self updateVertexBuffer]; 145 | } 146 | 147 | - (void) viewWillMoveToSuperview:(NSView *)newSuperview { 148 | _hasRendered = NO; 149 | [super viewWillMoveToSuperview:newSuperview]; 150 | if (!newSuperview) { 151 | [self.class.timerTargets removeObject:self]; 152 | } 153 | } 154 | 155 | - (id) initWithCoder:(NSCoder *)decoder { 156 | return [[super initWithCoder:decoder] commonInit]; 157 | } 158 | 159 | - (id) initWithFrame:(CGRect)frame { 160 | return [[super initWithFrame:frame] commonInit]; 161 | } 162 | 163 | + (id ) preferredDevice { 164 | NSArray > *devices = MTLCopyAllDevices(); 165 | for (id device in devices) { 166 | if (device.lowPower) { 167 | return device; 168 | } 169 | } 170 | 171 | return MTLCreateSystemDefaultDevice(); 172 | } 173 | 174 | + (id ) preferredCommandQueue { 175 | static dispatch_once_t onceToken; 176 | static id commandQueue; 177 | dispatch_once(&onceToken, ^{ 178 | commandQueue = [[self preferredDevice] newCommandQueue]; // WithMaxCommandBufferCount:100 179 | }); 180 | return commandQueue; 181 | } 182 | 183 | + (id ) preferredRenderPipeline { 184 | static dispatch_once_t onceToken; 185 | static id renderPipeline; 186 | dispatch_once(&onceToken, ^{ 187 | id device = [self preferredDevice]; 188 | id library = [device newDefaultLibrary]; 189 | MTLRenderPipelineDescriptor *pipelineDescriptor = [MTLRenderPipelineDescriptor new]; 190 | pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"vertex_main"]; 191 | pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"fragment_main"]; 192 | pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; 193 | renderPipeline = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor 194 | error:NULL]; 195 | }); 196 | return renderPipeline; 197 | } 198 | 199 | - (void) updateVertexBuffer { 200 | [self updateVertexBufferFromIndex:0 toIndex:(_numberOfValues - 1)]; 201 | } 202 | 203 | - (void) updateVertexBufferFromIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex { 204 | MTSVertex *vertices = (MTSVertex *)_vertexBuffer.contents; 205 | float_t minX = -1.0f, maxX = 1.0f, minY = -1.0f, maxY = 1.0f; 206 | float_t stepX = (maxX - minX) / (float_t)_numberOfValues; 207 | 208 | NSCParameterAssert(fromIndex >= 0); 209 | NSCParameterAssert(toIndex > fromIndex); 210 | NSCParameterAssert(toIndex < _numberOfValues); 211 | 212 | for (size_t i = fromIndex; i <= toIndex; i++) { 213 | float_t const value = _values[i]; 214 | 215 | BOOL const isOnBandBoundary = fmodf(fabsf(value), 0.25f) == 0.0f; 216 | BOOL const isPositive = (value > 0.0f); 217 | BOOL const isNegative = (value < 0.0f); 218 | 219 | float_t fromX = minX + (float_t)i * stepX; 220 | float_t toX = fromX + stepX; 221 | float_t midY = isOnBandBoundary ? 0.0f : 222 | isPositive ? 223 | (minY + (maxY - minY) * (fmodf(value, 0.25f) / 0.25f)) : 224 | (maxY - (maxY - minY) * (fmodf(fabsf(value), 0.25f) / 0.25f)); 225 | 226 | int bandNumber = bandNumberForValue(value); 227 | int bandIndex = abs(bandNumber) - 1; 228 | NSCParameterAssert(minY <= midY && midY <= maxY); 229 | NSCParameterAssert(-4 <= bandNumber && bandNumber <= 4); 230 | NSCParameterAssert((!isPositive && !isNegative) || ((0 < abs(bandNumber) && abs(bandNumber) <= 4))); 231 | 232 | vector_short3 topColor = { 255, 255, 255 }; 233 | vector_short3 bottomColor = { 255, 255, 255 }; 234 | 235 | if (isPositive) { 236 | bottomColor = positiveColors[bandIndex]; 237 | if (bandIndex) { 238 | topColor = isOnBandBoundary ? bottomColor : positiveColors[bandIndex - 1]; 239 | } 240 | } else if (isNegative) { 241 | topColor = negativeColors[bandIndex]; 242 | if (bandIndex) { 243 | bottomColor = isOnBandBoundary ? topColor : negativeColors[bandIndex - 1]; 244 | } 245 | } 246 | 247 | size_t offset = 8 * i; 248 | vertices[offset + 0] = (MTSVertex){ (vector_float2){ toX, maxY }, topColor }; 249 | vertices[offset + 1] = (MTSVertex){ (vector_float2){ toX, midY }, topColor }; 250 | vertices[offset + 2] = (MTSVertex){ (vector_float2){ fromX, maxY }, topColor }; 251 | vertices[offset + 3] = (MTSVertex){ (vector_float2){ fromX, midY }, topColor }; 252 | vertices[offset + 4] = (MTSVertex){ (vector_float2){ toX, midY }, bottomColor }; 253 | vertices[offset + 5] = (MTSVertex){ (vector_float2){ toX, minY }, bottomColor }; 254 | vertices[offset + 6] = (MTSVertex){ (vector_float2){ fromX, midY }, bottomColor }; 255 | vertices[offset + 7] = (MTSVertex){ (vector_float2){ fromX, minY }, bottomColor }; 256 | } 257 | } 258 | 259 | - (id) commonInit { 260 | self.device = self.class.preferredDevice; 261 | self.paused = YES; 262 | self.enableSetNeedsDisplay = YES; 263 | self.colorPixelFormat = MTLPixelFormatBGRA8Unorm; 264 | 265 | _numberOfValues = 128; 266 | _values = malloc(_numberOfValues * sizeof(float_t)); 267 | _values[0] = (2.0f * ((float_t)rand()/(float_t)RAND_MAX) - 1.0f); 268 | for (size_t i = 1; i < _numberOfValues; i++) { 269 | _values[i] = nextValue(_values[i - 1]); 270 | } 271 | 272 | NSUInteger bufferSize = sizeof(MTSVertex) * 4 * 2 * _numberOfValues; 273 | _vertexBuffer = [self.device newBufferWithLength:bufferSize options:MTLResourceStorageModeShared]; 274 | _renderPipeline = [self.class preferredRenderPipeline]; 275 | 276 | [self.class setupDisplayLink]; 277 | [self updateVertexBuffer]; 278 | 279 | return self; 280 | } 281 | 282 | - (void) drawRect:(NSRect)dirtyRect { 283 | [super drawRect:dirtyRect]; 284 | [self render]; 285 | if (!_hasRendered) { 286 | _hasRendered = YES; 287 | [self.class.timerTargets addObject:self]; 288 | } 289 | } 290 | 291 | - (void) render { 292 | MTLRenderPassDescriptor *currentRenderPassDescriptor = self.currentRenderPassDescriptor; 293 | if (!currentRenderPassDescriptor) 294 | return; 295 | 296 | id commandBuffer = [self.class.preferredCommandQueue commandBufferWithUnretainedReferences]; 297 | id renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:currentRenderPassDescriptor]; 298 | 299 | [renderCommandEncoder setRenderPipelineState:_renderPipeline]; 300 | [renderCommandEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:0]; 301 | for (size_t i = 0; i < _numberOfValues; i++) { 302 | [renderCommandEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:(8 * i) vertexCount:4]; 303 | [renderCommandEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:(8 * i) + 4 vertexCount:4]; 304 | } 305 | 306 | [renderCommandEncoder endEncoding]; 307 | [commandBuffer presentDrawable:self.currentDrawable]; 308 | [commandBuffer commit]; 309 | } 310 | 311 | @end 312 | -------------------------------------------------------------------------------- /metrics/MultiLayerTimeSeriesView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MultiLayerTimeSeriesView.h 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 01/01/2017. 6 | // Copyright © 2017 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MultiLayerTimeSeriesView : NSView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /metrics/MultiLayerTimeSeriesView.m: -------------------------------------------------------------------------------- 1 | // 2 | // MultiLayerTimeSeriesView.m 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 01/01/2017. 6 | // Copyright © 2017 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "MultiLayerTimeSeriesView.h" 12 | 13 | @interface MultiLayerTimeSeriesView () 14 | + (NSMutableSet *) timerTargets; 15 | + (CVDisplayLinkRef) displayLink; 16 | - (void) tick:(const CVTimeStamp *)outputTime; 17 | @property (nonatomic, readonly, strong) NSMutableArray *values; 18 | @end 19 | 20 | static CVReturn MultiLayerTimeSeriesViewCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) { 21 | NSArray *targets = [MultiLayerTimeSeriesView.timerTargets copy]; 22 | 23 | dispatch_sync(dispatch_get_main_queue(), ^{ 24 | [CATransaction begin]; 25 | [CATransaction setDisableActions:YES]; 26 | for (MultiLayerTimeSeriesView *target in targets) { 27 | [target tick:outputTime]; 28 | } 29 | [CATransaction commit]; 30 | }); 31 | 32 | 33 | return kCVReturnSuccess; 34 | } 35 | 36 | @implementation MultiLayerTimeSeriesView 37 | 38 | + (NSMutableSet *) timerTargets { 39 | static NSMutableSet *timerTargets = nil; 40 | static dispatch_once_t onceToken; 41 | dispatch_once(&onceToken, ^{ 42 | timerTargets = [NSMutableSet set]; 43 | }); 44 | 45 | return timerTargets; 46 | } 47 | 48 | + (CVDisplayLinkRef) displayLink { 49 | static CVDisplayLinkRef displayLink = nil; 50 | static dispatch_once_t onceToken; 51 | 52 | dispatch_once(&onceToken, ^{ 53 | CVDisplayLinkRef displayLink = NULL; 54 | CVDisplayLinkCreateWithActiveCGDisplays(&displayLink); 55 | CVDisplayLinkSetOutputCallback(displayLink, &MultiLayerTimeSeriesViewCallback, (__bridge void *)self); 56 | CVDisplayLinkStart(displayLink); 57 | }); 58 | 59 | return displayLink; 60 | } 61 | 62 | + (NSArray *)colors { 63 | static dispatch_once_t onceToken; 64 | static NSArray *colors; 65 | dispatch_once(&onceToken, ^{ 66 | NSColor * (^colourFromComponents)(NSUInteger, NSUInteger, NSUInteger) = ^ (NSUInteger red, NSUInteger green, NSUInteger blue) { 67 | return [NSColor colorWithSRGBRed:(((float_t)red)/256.0f) green:(((float_t)green)/256.0f) blue:(((float_t)blue)/256.0f) alpha:1.0f]; 68 | }; 69 | 70 | colors = @[ 71 | colourFromComponents(5,112,176), 72 | colourFromComponents(116,169,207), 73 | colourFromComponents(189,201,225), 74 | colourFromComponents(241,238,246), 75 | colourFromComponents(237,248,251), 76 | colourFromComponents(178,226,226), 77 | colourFromComponents(102,194,164), 78 | colourFromComponents(35,139,69) 79 | ]; 80 | }); 81 | 82 | return colors; 83 | } 84 | 85 | + (CGImageRef) sharedStripeImage { 86 | static dispatch_once_t onceToken; 87 | static CGImageRef stripeImage; 88 | dispatch_once(&onceToken, ^{ 89 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 90 | uint32_t bitmapInfo = kCGImageAlphaNoneSkipFirst|kCGBitmapByteOrderDefault; 91 | size_t stepHeight = 32; 92 | size_t bufferWidth = 1; 93 | size_t bufferHeight = stepHeight * self.colors.count; 94 | CGContextRef context = CGBitmapContextCreate(NULL, bufferWidth, bufferHeight, 8, bufferWidth * 8, colorSpace, bitmapInfo); 95 | CGColorSpaceRelease(colorSpace); 96 | 97 | NSUInteger colorIndex = 0; 98 | for (NSColor *color in self.colors) { 99 | CGContextSetFillColorWithColor(context, color.CGColor); 100 | CGContextFillRect(context, (NSRect){ 0, stepHeight * colorIndex, bufferWidth, stepHeight }); 101 | colorIndex = colorIndex + 1; 102 | } 103 | stripeImage = CGBitmapContextCreateImage(context); 104 | CGContextRelease(context); 105 | }); 106 | return stripeImage; 107 | } 108 | 109 | - (id) initWithCoder:(NSCoder *)decoder { 110 | return [[super initWithCoder:decoder] commonInit]; 111 | } 112 | 113 | - (id) initWithFrame:(CGRect)frame { 114 | return [[super initWithFrame:frame] commonInit]; 115 | } 116 | 117 | - (id) commonInit { 118 | self.layer = [CALayer layer]; 119 | self.layer.backgroundColor = [NSColor whiteColor].CGColor; 120 | self.layer.opaque = YES; 121 | self.layer.masksToBounds = NO; 122 | self.layer.edgeAntialiasingMask = 0; 123 | self.layer.delegate = self; 124 | self.wantsLayer = YES; 125 | self.postsFrameChangedNotifications = YES; 126 | self.postsBoundsChangedNotifications = YES; 127 | 128 | _values = [NSMutableArray arrayWithCapacity:128]; 129 | return self; 130 | } 131 | 132 | - (void) viewWillMoveToSuperview:(NSView *)newSuperview { 133 | [super viewWillMoveToSuperview:newSuperview]; 134 | if (newSuperview) { 135 | [self.class displayLink]; 136 | [self.class.timerTargets addObject:self]; 137 | } else { 138 | self.postsFrameChangedNotifications = NO; 139 | self.postsBoundsChangedNotifications = NO; 140 | [self.class.timerTargets removeObject:self]; 141 | } 142 | } 143 | 144 | - (CGFloat) stepWidth { 145 | return 1.0f; 146 | } 147 | 148 | - (void) recreateSublayers { 149 | NSArray *existingLayers = self.layer.sublayers; 150 | NSUInteger numberOfExistingLayers = existingLayers.count; 151 | NSUInteger numberOfRequiredLayers = CGRectGetWidth(self.bounds) / self.stepWidth; 152 | if (numberOfExistingLayers > numberOfRequiredLayers) { 153 | NSArray *removedLayers = [existingLayers objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, numberOfRequiredLayers - numberOfExistingLayers)]]; 154 | for (CALayer *layer in removedLayers) { 155 | [layer removeFromSuperlayer]; 156 | } 157 | } else if (numberOfExistingLayers < numberOfRequiredLayers) { 158 | NSUInteger numberOfAdditionalLayers = numberOfRequiredLayers - numberOfExistingLayers; 159 | for (NSUInteger i = 0; i < numberOfAdditionalLayers; i++) { 160 | [self.layer addSublayer:[self newLayer]]; 161 | } 162 | } 163 | } 164 | 165 | - (void) layoutSublayersOfLayer:(CALayer *)layer { 166 | if (layer != self.layer) { 167 | return; 168 | } 169 | [self recreateSublayers]; 170 | 171 | CGFloat stepWidth = self.stepWidth; 172 | CGFloat stageWidth = CGRectGetWidth(self.bounds); 173 | CGFloat stageHeight = CGRectGetHeight(self.bounds); 174 | 175 | NSUInteger layerIndex = 0; 176 | for (CALayer *sublayer in self.layer.sublayers) { 177 | sublayer.bounds = (CGRect){ 0, 0, stepWidth, 8.0f * stageHeight }; 178 | sublayer.backgroundColor = [NSColor colorWithRed:((CGFloat)layerIndex / stageWidth) green:1 blue:1 alpha:1].CGColor; 179 | layerIndex = layerIndex + 1; 180 | } 181 | } 182 | 183 | - (CALayer *) newLayer { 184 | CALayer *layer = [CALayer layer]; 185 | // layer.anchorPoint = CGPointZero; 186 | layer.backgroundColor = [NSColor redColor].CGColor; 187 | // layer.contents = (__bridge id)self.class.sharedStripeImage; 188 | // layer.contentsRect = (CGRect) { 0, 0, 1, 0.125f }; 189 | // layer.contentsScale = 1.0f; 190 | return layer; 191 | } 192 | 193 | - (void) fillValue { 194 | NSNumber *lastNumber; 195 | float_t target = ((lastNumber = self.values.lastObject)) ? 196 | (lastNumber.floatValue + (0.1f * ((float)rand()/(float)RAND_MAX) - 0.05f)) : 197 | (2.0f * ((float)rand()/(float)RAND_MAX) - 1.0f); 198 | [self.values addObject:@(MIN(1.0f, MAX(-1.0f, target)))]; 199 | 200 | if (self.values.count > 128) { 201 | [self.values removeObjectsInRange:(NSRange){ 0, self.values.count - 128 }]; 202 | } 203 | } 204 | 205 | - (void) tick:(const CVTimeStamp *)outputTime { 206 | [self fillValue]; 207 | 208 | NSArray *values = self.values; 209 | NSArray *layers = self.layer.sublayers; 210 | NSUInteger numberOfValuesShown = MIN(layers.count, values.count); 211 | 212 | if (numberOfValuesShown == 0) 213 | return; 214 | 215 | NSArray *valuesShown = [values objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(values.count - numberOfValuesShown,numberOfValuesShown)]]; 216 | 217 | NSArray *layersAdjusted = [layers objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(layers.count - valuesShown.count, valuesShown.count)]]; 218 | 219 | CGFloat stepWidth = self.stepWidth; 220 | CGFloat stageWidth = CGRectGetWidth(self.bounds); 221 | CGFloat stageHeight = CGRectGetHeight(self.bounds); 222 | NSUInteger index = 0; 223 | 224 | // NSLog(@"tick %i/%i %i/%i", values.count, valuesShown.count, layers.count, layersAdjusted.count); 225 | for (CALayer *layer in layersAdjusted) { 226 | layer.position = (CGPoint){ 227 | (stageWidth - (CGFloat)index * stepWidth), 228 | valuesShown[index].floatValue / stageHeight 229 | }; 230 | // NSLog(@"-> %@", NSStringFromPoint(layer.position)); 231 | index = index + 1; 232 | // layer.contentsRect = (CGRect) { 233 | // 0, 234 | // valuesShown[index].floatValue / 9.0f, 235 | // 1, 236 | // 0.125f 237 | // }; 238 | } 239 | } 240 | 241 | @end 242 | -------------------------------------------------------------------------------- /metrics/RATilingBackgroundView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RATilingBackgroundView.h 3 | // RATilingBackgroundView 4 | // 5 | // Created by Evadne Wu on 11/6/12. 6 | // Copyright (c) 2012 Radius. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "RATilingBackgroundViewDelegate.h" 11 | 12 | @interface RATilingBackgroundView : NSView 13 | 14 | @property (nonatomic, readwrite, assign) BOOL horizontalStretchingEnabled; // YES 15 | @property (nonatomic, readwrite, assign) BOOL verticalStretchingEnabled; // NO 16 | @property (nonatomic, readwrite, weak) IBOutlet id delegate; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /metrics/RATilingBackgroundView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RATilingBackgroundView.m 3 | // RATilingBackgroundView 4 | // 5 | // Created by Evadne Wu on 11/6/12. 6 | // Copyright (c) 2012 Radius. All rights reserved. 7 | // 8 | 9 | #import "RATilingBackgroundView.h" 10 | 11 | typedef struct RATilePlacement { 12 | NSUInteger x; 13 | NSUInteger y; 14 | CGRect rect; 15 | __unsafe_unretained NSView *tile; 16 | } RATilePlacement; 17 | 18 | @interface RATilingBackgroundView () 19 | @property (nonatomic, readwrite, assign) RATilePlacement *previousTilePlacements; 20 | @property (nonatomic, readwrite, assign) CGPoint offset; 21 | @property (nonatomic, readonly, strong) NSMutableArray *visibleTiles; 22 | @property (nonatomic, readonly, strong) NSMutableArray *dequeuedTiles; 23 | - (void) reset; 24 | - (void) setUpObservations; 25 | - (void) tearDownObservations; 26 | @end 27 | 28 | @implementation RATilingBackgroundView 29 | 30 | - (BOOL) isFlipped { 31 | return YES; 32 | } 33 | 34 | - (id) initWithCoder:(NSCoder *)decoder { 35 | self = [super initWithCoder:decoder]; 36 | if (!self) { 37 | return nil; 38 | } 39 | 40 | [self commonInit]; 41 | _horizontalStretchingEnabled = [decoder decodeBoolForKey:@"horizontalStretchingEnabled"]; 42 | _verticalStretchingEnabled = [decoder decodeBoolForKey:@"verticalStretchingEnabled"]; 43 | return self; 44 | } 45 | 46 | - (id) initWithFrame:(CGRect)frame { 47 | self = [super initWithFrame:frame]; 48 | if (!self) { 49 | return nil; 50 | } 51 | 52 | [self commonInit]; 53 | return self; 54 | } 55 | 56 | - (BOOL) wantsDefaultClipping { 57 | return NO; 58 | } 59 | 60 | - (void) commonInit { 61 | _horizontalStretchingEnabled = YES; 62 | _verticalStretchingEnabled = NO; 63 | _offset = CGPointZero; 64 | _visibleTiles = [NSMutableArray array]; 65 | _dequeuedTiles = [NSMutableArray array]; 66 | // self.layer = [CALayer layer]; 67 | // self.layer.shouldRasterize = YES; 68 | // self.wantsLayer = YES; 69 | // self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawNever; 70 | } 71 | 72 | - (void) encodeWithCoder:(NSCoder *)coder { 73 | [super encodeWithCoder:coder]; 74 | [coder encodeBool:_horizontalStretchingEnabled forKey:@"horizontalStretchingEnabled"]; 75 | [coder encodeBool:_verticalStretchingEnabled forKey:@"verticalStretchingEnabled"]; 76 | } 77 | 78 | - (void) viewWillMoveToSuperview:(NSView *)newSuperview { 79 | [super viewWillMoveToSuperview:newSuperview]; 80 | if (self.superview) { 81 | [self tearDownObservations]; 82 | } 83 | } 84 | 85 | - (void) viewDidMoveToSuperview { 86 | [super viewDidMoveToSuperview]; 87 | [self reset]; 88 | if (self.superview) { 89 | [self setUpObservations]; 90 | if (!self.subviews.count) { 91 | [self resizeSubviewsWithOldSize:CGSizeZero]; 92 | } 93 | } 94 | } 95 | 96 | - (void) setDelegate:(id)delegate { 97 | if (_delegate != delegate) { 98 | _delegate = delegate; 99 | [self resizeSubviewsWithOldSize:self.frame.size]; 100 | } 101 | } 102 | 103 | - (void) resizeSubviewsWithOldSize:(NSSize)oldSize { 104 | if (!self.delegate) { 105 | return; 106 | } 107 | 108 | NSCParameterAssert(self.delegate); 109 | [super resizeSubviewsWithOldSize:oldSize]; 110 | 111 | if (!(CGRectGetWidth(self.bounds))) 112 | return; 113 | 114 | if (!(CGRectGetHeight(self.bounds))) 115 | return; 116 | 117 | NSPointerArray *unusedVisibleTiles = [NSPointerArray weakObjectsPointerArray]; 118 | for (NSView *visibleTile in self.visibleTiles) { 119 | [unusedVisibleTiles addPointer:(void *)visibleTile]; 120 | } 121 | 122 | NSUInteger tileRectsCount = 0; 123 | [self getPrimitiveTilingRects:NULL count:&tileRectsCount]; 124 | CGRect * const tileRects = malloc(tileRectsCount * sizeof(CGRect)); 125 | memset(tileRects, 0, tileRectsCount * sizeof(CGRect)); 126 | [self getPrimitiveTilingRects:tileRects count:&tileRectsCount]; 127 | 128 | NSUInteger const unusedVisibleTilesCount = [unusedVisibleTiles count]; 129 | 130 | for (NSUInteger tileRectIndex = 0; tileRectIndex < tileRectsCount; tileRectIndex++) { 131 | CGRect rect = tileRects[tileRectIndex]; 132 | NSCParameterAssert(rect.size.width > 0); 133 | NSCParameterAssert(rect.size.height > 0); 134 | NSView *tile = nil; 135 | 136 | if (!!unusedVisibleTilesCount && (tileRectIndex <= (unusedVisibleTilesCount - 1))) { 137 | tile = (NSView *)[unusedVisibleTiles pointerAtIndex:tileRectIndex]; 138 | [unusedVisibleTiles replacePointerAtIndex:tileRectIndex withPointer:NULL]; 139 | } else { 140 | tile = [self newTile]; 141 | [self addSubview:tile]; 142 | [self.visibleTiles addObject:tile]; 143 | } 144 | tile.frame = rect; 145 | } 146 | 147 | free(tileRects); 148 | NSArray *leftoverTiles = [unusedVisibleTiles allObjects]; 149 | 150 | [self.visibleTiles removeObjectsInArray:leftoverTiles]; 151 | [self.dequeuedTiles addObjectsFromArray:leftoverTiles]; 152 | 153 | for (NSView *unusedVisibleTile in leftoverTiles) { 154 | [unusedVisibleTile removeFromSuperview]; 155 | } 156 | } 157 | 158 | - (CGSize) tileSize { 159 | CGSize requestedTileSize = [self.delegate sizeForTilesInTilingBackgroundView:self]; 160 | return (CGSize){ 161 | .width = self.horizontalStretchingEnabled ? 162 | CGRectGetWidth(self.bounds) : 163 | requestedTileSize.width, 164 | .height = self.verticalStretchingEnabled ? 165 | CGRectGetHeight(self.bounds) : 166 | requestedTileSize.height 167 | }; 168 | } 169 | 170 | - (void) getPrimitiveTilingRects:(CGRect *)outRects count:(NSUInteger *)outCount { 171 | NSCParameterAssert(outCount); 172 | 173 | CGSize const tileSize = self.tileSize; 174 | CGSize const boundsSize = (CGSize){ 175 | .width = CGRectGetWidth(self.bounds), 176 | .height = CGRectGetHeight(self.bounds) 177 | }; 178 | 179 | CGFloat const stepX = tileSize.width; 180 | CGFloat const fromX = fmodf(self.offset.x, stepX); 181 | CGFloat const toX = boundsSize.width; 182 | CGFloat const stepY = tileSize.height; 183 | CGFloat const fromY = fmodf(self.offset.y, stepY); 184 | CGFloat const toY = boundsSize.height; 185 | 186 | NSUInteger const numberOfTilesX = (NSUInteger)(ceilf(toX / stepX) - floorf(fromX / stepX)); 187 | NSUInteger const numberOfTilesY = (NSUInteger)(ceilf(toY / stepY) - floorf(fromY / stepY)); 188 | NSUInteger const numberOfTiles = numberOfTilesX * numberOfTilesY; 189 | 190 | if (outCount) { 191 | *outCount = numberOfTiles; 192 | } 193 | 194 | if (!outRects) { 195 | return; 196 | } 197 | 198 | NSUInteger rectIndex = 0; 199 | for (NSUInteger indexX = 0; indexX < numberOfTilesX; indexX++) { 200 | for (NSUInteger indexY = 0; indexY < numberOfTilesY; indexY++) { 201 | NSCParameterAssert(rectIndex < numberOfTiles); 202 | outRects[rectIndex] = (CGRect){ 203 | .origin.x = fromX + indexX * stepX, 204 | .origin.y = fromY + indexY * stepY, 205 | .size = tileSize 206 | }; 207 | rectIndex++; 208 | } 209 | } 210 | } 211 | 212 | - (NSView *) newTile { 213 | NSMutableArray *dequeuedTiles = self.dequeuedTiles; 214 | NSView *tile = [dequeuedTiles count] ? 215 | [dequeuedTiles objectAtIndex:0] : 216 | nil; 217 | 218 | if (tile) { 219 | [dequeuedTiles removeObject:tile]; 220 | return tile; 221 | } else { 222 | NSView *newTile = [self.delegate newTileForTilingBackgroundView:self]; 223 | newTile.autoresizingMask = NSViewNotSizable; 224 | return newTile; 225 | } 226 | } 227 | 228 | - (void) reset { 229 | self.offset = CGPointZero; 230 | if (self.delegate) { 231 | [self resizeSubviewsWithOldSize:self.frame.size]; 232 | } 233 | } 234 | 235 | - (void) setUpObservations { 236 | NSView * const target = self.superview; 237 | NSKeyValueObservingOptions const options = NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew; 238 | void * const context = (__bridge void *)self; 239 | 240 | NSCParameterAssert(target); 241 | 242 | if ([target respondsToSelector:NSSelectorFromString(@"contentOffset")]) { 243 | [target addObserver:self forKeyPath:@"contentOffset" options:options context:context]; 244 | } 245 | 246 | [target addObserver:self forKeyPath:@"bounds" options:options context:context]; 247 | [target addObserver:self forKeyPath:@"frame" options:options context:context]; 248 | } 249 | 250 | - (void) tearDownObservations { 251 | void * const context = (__bridge void *)self; 252 | NSView * const target = self.superview; 253 | 254 | NSCParameterAssert(target); 255 | 256 | if ([target respondsToSelector:NSSelectorFromString(@"contentOffset")]) { 257 | [target removeObserver:self forKeyPath:@"contentOffset" context:context]; 258 | } 259 | 260 | [target removeObserver:self forKeyPath:@"bounds" context:context]; 261 | [target removeObserver:self forKeyPath:@"frame" context:context]; 262 | } 263 | 264 | - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 265 | [self setNeedsLayout:YES]; 266 | if (object == self.superview) { 267 | if ([keyPath isEqualToString:@"contentOffset"]) { 268 | CGPoint toContentOffset = [change[NSKeyValueChangeNewKey] CGPointValue]; 269 | CGSize tileSize = [self tileSize]; 270 | self.offset = (CGPoint){ 271 | fmodf(-1 * toContentOffset.x, tileSize.width) - 272 | (self.horizontalStretchingEnabled ? 273 | 0.0f : 274 | ceilf(CGRectGetWidth(self.bounds) / tileSize.width) * tileSize.width), 275 | fmodf(-1 * toContentOffset.y, tileSize.height) - 276 | (self.verticalStretchingEnabled ? 277 | 0.0f : 278 | ceilf(CGRectGetHeight(self.bounds) / tileSize.height) * tileSize.height) 279 | }; 280 | } else { 281 | self.frame = self.superview.bounds; 282 | } 283 | } 284 | } 285 | 286 | @end 287 | -------------------------------------------------------------------------------- /metrics/RATilingBackgroundViewDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // RATilingBackgroundViewDelegate.h 3 | // RATilingBackgroundView 4 | // 5 | // Created by Evadne Wu on 11/6/12. 6 | // Copyright (c) 2012 Radius. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class RATilingBackgroundView; 12 | @protocol RATilingBackgroundViewDelegate 13 | 14 | - (CGSize) sizeForTilesInTilingBackgroundView:(RATilingBackgroundView *)tilingBackgroundView; 15 | - (NSView *) newTileForTilingBackgroundView:(RATilingBackgroundView *)tilingBackgroundView; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /metrics/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // metrics 4 | // 5 | // Created by Evadne Wu on 21/12/2016. 6 | // Copyright © 2016 Radius Development. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | return NSApplicationMain(argc, argv); 13 | } 14 | --------------------------------------------------------------------------------