Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Welcome to SoundCloud!

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

Welcome to SoundCloud!

Avatar for Alexander Kovalev

Alexander Kovalev

October 03, 2013

Other Decks in Programming

Transcript

  1. SOUNDCLOUD SoundCloud • Social audio platform • 55 mln users

    and growing • 240 mln unique visitors a month • 12 hours of audio are uploaded every minute • 15 engineering teams Wednesday, October 9, 13
  2. SOUNDCLOUD Quick assumptions • Awareness of MVC pattern • you

    know that SPA is not a mineral spring • why and when SPA makes a difference • you heard of web app frameworks Wednesday, October 9, 13
  3. SOUNDCLOUD What is NEXT? • default production app since December,

    2012 • SoundCloud API client • the long running app • ~45,000 LOC, 80% of that is JS • supports only modern browsers • better experience Wednesday, October 9, 13
  4. SOUNDCLOUD Our Stack • Require.js (Almond.js) • Node.js • Uglify.js/Esprima

    • Klang.js • Backbone (Trombone) • Handlebars (with plenty of helpers) • Home-grown development tools Wednesday, October 9, 13
  5. SOUNDCLOUD Backbone • small footprint • Observable mixin • doesn’t

    depend on third-party libraries • helpful methods for data manipulation • easy to understand and extend • but not opinionated about app structure Wednesday, October 9, 13
  6. SOUNDCLOUD Trombone > Backbone • dev tools • build tools

    • core view component • view life cycle management • layout management • data management More details here - snd.sc/16i6mhU Wednesday, October 9, 13
  7. SOUNDCLOUD Trombone Views — they do a lot • define

    a standard view declaration • rendering of templates • establish a data source • support the nested views • setup and teardown event listeners • don’t depend on the context Wednesday, October 9, 13
  8. SOUNDCLOUD View declaration in Trombone var Sound = require('models/sound'); var

    View = require('lib/view'); var WaveformView = module.exports = View.extend({ template: require('views/sound/waveform.tmpl'), css: require('views/sound/waveform.css'), ModelClass: Sound, requiredAttributes: [‘waveform_url’] }); Wednesday, October 9, 13
  9. SOUNDCLOUD Write CSS but serve AMD .myView { padding: 5px;

    color: #f0f; } .myView__foo { border: 1px solid #0f0; } Result is an AMD module define("views/myView.css", [], function (require, exports, module) { var style = module.exports = document.createElement('style'); style.appendChild( document.createTextNode('.myView { padding: 5px; color ... } ...'); ); }) Wednesday, October 9, 13
  10. SOUNDCLOUD Write CSS but serve AMD var WaveformView = module.exports

    = View.extend({ // it’s a `style` element css: require('views/sound/waveform.css'), render: function() { if (!this.css.parentNode) { document.body.appendChild(this.css); } // ... } }); Wednesday, October 9, 13
  11. SOUNDCLOUD Nested views <h1>{{title}}</h1> {{view "view/waveform" id=123}} var Waveform =

    require('views/waveform'); var waveform = new Waveform({ resource_id: 123 }); soundView.addSubview(waveform); soundView.$el.append(waveform.render().el); You can add subviews both in a template and in js file Wednesday, October 9, 13
  12. SOUNDCLOUD Looks good, right? var Sound = require('models/sound'); var View

    = require('lib/view'); var WaveformView = module.exports = View.extend({ template: require('views/sound/waveform.tmpl'), css: require('views/sound/waveform.css'), ModelClass: Sound, initialize: function(args) { // new instance is created for each view this.model = new this.ModelClass({ id: args.id }); this.setupModelListeners(); } }); Wednesday, October 9, 13
  13. SOUNDCLOUD Identity Map IdentityMap.applyTo(Sound, { hashFn: function(attrs) { return attrs.id

    || null; } }); var s1 = new Sound({ id: 123, genre: 'Ambient' }) , s2 = new Sound({ id: 123, artist: 'Brian Eno’ }); s1 === s2; // true - these are the exact same objects. Wednesday, October 9, 13
  14. SOUNDCLOUD Don’t use it in production :) var store =

    {}; Sound = function(attributes.id) { var id = attributes.id; // check if this model has already been created if (store[id]) { return store[id]; // ← return the other one } // otherwise, store this instance store[id] = this; // regular instantiation... this.initialize(); } }; Wednesday, October 9, 13
  15. SOUNDCLOUD IdentityMap is applied to subclasses IdentityMap.applyTo(SoundModel, { hashFn: function(attrs)

    { return attrs.id || null; } }); // Subclasses inherit the “IdentityMap” behaviour PromoSound = SoundModel.extend({}); s1 = new PromoSound({id : 123}), s2 = new PromoSound({id : 123}); s1 === s2; // true Wednesday, October 9, 13
  16. SOUNDCLOUD Identity Map var s1 = new Sound({ id: 123,

    genre: 'Ambient' }) , s2 = new Sound({ id: 123, artist: 'Brian Eno’ }); s1 === s2; // true - these are the exact same objects. s1.get('genre'); // 'Ambient' s2.get('artist'); // 'Music for Airports' Wednesday, October 9, 13
  17. SOUNDCLOUD Wait a second var store = {}; Sound =

    function(attrs) { var id = attributes.id; // check if this model has already been created if (store[id]) { return store[id]; } // otherwise, store this instance store[id] = this; // ← it’s a massive a memory leak // regular instantiation... this.initialize(); } }; Wednesday, October 9, 13
  18. SOUNDCLOUD Release it! var map = {}, counts = {};

    // <-- usage counter function Sound(id) { if (map[id]) { ++counts[id]; return map[id]; } map[id] = this; counts[id] = 1; this.initialize(); } Sound.prototype.release = function () { --counts[this.id]; } Wednesday, October 9, 13
  19. SOUNDCLOUD Browsers are getting better function leak() { var A

    = {}, B = {}; A.b = B; B.a = A; } // Circular reference does exist // but A and B will be cleaned up // as you can't reach either from global scope leak(); Wednesday, October 9, 13
  20. SOUNDCLOUD No longer used != No longer reachable var View

    = require('lib/view'), Sound = require('models/sound'); var WaveformView = module.exports = View.extend({ setup: function() { this.sound = new SoundModel({ id: 123 }); }, // sound model is not disposed properly later on // as we mistakenly redefined `dispose` method dispose: function() {} }); Wednesday, October 9, 13
  21. SOUNDCLOUD Static analysis tools to the rescue $ ./tools/memory-leak-finder views/waveform.js

    Warnings: 1 Sound model is not disposed. Release it in `dispose` method. A very nice read - http://research.google.com/pubs/pub40738.html Global leaking detector - https://github.com/kesla/node-leaky Wednesday, October 9, 13
  22. SOUNDCLOUD Monitor memory info window.peformance.memory = { // memory JS

    heap is limited to jsHeapSizeLimit : 793000000, // memory allocated by JS (including free space) usedJSHeapSize : 18200000, // memory currently being used totalJSHeapSize : 39600000 } setInterval(function() { if (usedJSHeapSize/jsHeapSizeLimit > 0.9) { console.log(‘He is almost dead, Jim!’); } }, 30 * 1000); Wednesday, October 9, 13
  23. SOUNDCLOUD Inline caching in V8 function Point(x, y, z) {

    // introduce all properties in the constructor // to enable “fast-object” mode. this.x = x; this.y = y; this.z = z; } var point = new Point(11, 22, 33); point 11 22 33 Point ‘x’ ‘y’ Point Point ‘x’ ‘y’ ‘z’ Point ‘x’ x y z Wednesday, October 9, 13
  24. SOUNDCLOUD Dictionary mode in V8 var point = new Point(11,

    22, 33); // forcing the object into dictionary mode delete point.z; // accessing a hash table is much slower // than accessing a field at a known offset. console.log(point.x); point 11 22 HashMap x : 11 y : 22 Wednesday, October 9, 13
  25. SOUNDCLOUD Less is more var FastSound = function (title, duration)

    { this.title = title; this.duration = duration; } var SlowSound = function (title, duration) { this.title = title; this.duration = duration; }; var fast = new FastSound('Hommage a Rameau', '350000'); var slow = new SlowSound('Hommage a Rameau', '350000'); delete slow.duration; Wednesday, October 9, 13
  26. SOUNDCLOUD Less is more Constructor Objects count Shallow size Retained

    size -------------------------------------------------------- SlowSound 10000 120 000 4 240 000 FastSound 10000 200 000 200 000 Slow object is using up 20 times more memory Wednesday, October 9, 13
  27. SOUNDCLOUD Trombone takes care of it View = module.exports =

    Backbone.View.extend({ ✂ ✂ ✂ ✂ ✂ dispose: function() { // remove event listeners this.teardownModelListeners(); this.teardownCollectionListeners(); // tell GC that we don't need these resources anymore this.model.release(); this.collection.release(); } ✂ ✂ ✂ ✂ ✂ }) Wednesday, October 9, 13
  28. SOUNDCLOUD Use Object Pools var soundPool = [], activeSounds =

    []; var sound = getSound(); ✂ releaseSound(sound); sound = null; function getSound() { var sound; if (soundPool.length) { sound = soundPool.pop(); sound.reset(); // important!!! } else { sound = new Sound(); } return activeSounds.push(sound); } function releaseSound(sound) { var index = activeSounds.indexOf(soundPool); if (index > -1) { activeSounds.splice(index, 1); } } Wednesday, October 9, 13
  29. SOUNDCLOUD Many men, many minds "SoundCloud users comments attached to

    songs are iconic and life changing" Wednesday, October 9, 13
  30. SOUNDCLOUD When N gets big Template-based approach • based on

    ListView component • list item is 4 nodes: li > a > img + span • 3 sounds: 4080 * 4 = 16,320 nodes • 272,000 nodes per stream (50 sounds) Wednesday, October 9, 13
  31. SOUNDCLOUD Gradual improvements • Most efficient detection algorithm • Cached

    scaled image • Typed Arrays • WebWorkers (Chromium Issue #51171) Wednesday, October 9, 13
  32. SOUNDCLOUD Think about what’s important • Moved calculation to the

    back end • Ported algorithm to Go • Front end recieves JSON, everyone happy Wednesday, October 9, 13
  33. SOUNDCLOUD Telemetry $ cd $CHROMIUM_SOURCE/tools/perf/page-sets $ cat soundcloud.json { "

    "description" : "Is it smooth enough?", " "smoothness" : { action : "scroll" }, " "pages""" : [ " " { "url" : "https://soundcloud.com/akovalev" } " ] } $ ../run-measurement smoothness soundcloud.json -o result.csv Telemetry is a Python wrapper around of the DevTools Remote Debugging Protocol Wednesday, October 9, 13
  34. SOUNDCLOUD Bazooka (An In House Heroku) $ git push bazooka

    stable remote: change-set accepted ... remote: building... ... remote: build finished Bazooka is a deployment solution for 12 factors apps - http://12factor.net/ More details on bazooka - goo.gl/pfSTAf Wednesday, October 9, 13
  35. SOUNDCLOUD Structure of Trombone project app !"" lib !"" models

    !"" collections !"" layouts !"" views !"" vendor !"" application.js #"" index.html var coreDeps = getDeps('application.js'), layoutSeeds = getLayoutSeeds('layouts'); writePackage(coreDeps); layoutSeeds.forEach(function(layoutSeed) { var layoutDeps = getLayoutDeps(layoutSeed); writePackage(layoutDeps); }); function getLayoutDeps(layoutSeed) { var layoutDeps = getDeps(layoutSeed); return _.difference(layoutDeps, coreDeps) } $ trombone build Wednesday, October 9, 13
  36. SOUNDCLOUD Building app !"" lib !"" models !"" collections !""

    layouts !"" views !"" vendor !"" application.js #"" index.html public !"" layouts $ !"" home-f3c2-842c8906.js $ !"" listen-2ED1-abef2203.js $ #"" stream-KJsr-540a7923.js #"" core !"" sc-wfJV-abef2203.js !"" vn-iAHt-e1684d31.js #"" tm-idSh-540a7923.js $ trombone build Wednesday, October 9, 13
  37. var client_id = __ENV__ === ‘prod’ ? ‘abc123’ : ‘def456’;

    if (__DEBUG_MODE__) { console.log(a); } uglify.ast_mangle(ast, { defines: { __ENV__ : ‘prod’, __DEBUG_MODE__ : false }}); uglify.ast_squeeze(ast); var a = ‘abc123’; Dead code removal Wednesday, October 9, 13
  38. AST is easy • Meaningful code vs Regex magic •

    Easy to change "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "soundId" }, "init": { "type": "Literal", "value": 123, "raw": "123" } } ] var soundId = 123; Wednesday, October 9, 13
  39. Esprima • ECMAScript parser written in ECMAScript • Accepts ECMAScriptcode

    and generates AST • Meaningful code vs Regex magic • compatible with Mozilla Parser AST • Harmony branch Wednesday, October 9, 13
  40. Esprima’s best friends escodegen escodegen AST Code estraverse estraverse.traverse(ast, {

    enter: function(node, parent) { }, leave: function(node, parent) { } }); SourceMap Wednesday, October 9, 13
  41. Function instrumentation function addLogging(code) { var ast = esprima.parse(code); estraverse.traverse(ast,

    { enter: function() { if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') { addBeforeCode(code); } } }); return escodegen.generate(ast); } function addBeforeCode(node) { var name = node.id ? node.id.name : '<anonymous function>', beforeCode = "console.log('Entering " + name + "()')" beforeNodes = esprima.parse(beforeCode).body; node.body.body = beforeNodes.concat(node.body.body); } Wednesday, October 9, 13
  42. Function instrumentation FunctionDeclaration Identifier BlockStatement Array Statement Statement ... Statement

    esprima estraverse escodegen Code AST Modified AST Code Wednesday, October 9, 13