├── .mailmap ├── src ├── images │ ├── favicon.ico │ ├── solver.gif │ ├── evil-kitty.gif │ ├── easy-solved.jpg │ ├── tota11y-logo.png │ ├── txn-timeline.png │ ├── txn-timeline.xcf │ ├── a11y-devtools.png │ ├── chameleon │ │ ├── cms.png │ │ ├── scratchpad-demo.gif │ │ └── partially-translated-exercise.png │ ├── early-tota11y.png │ ├── eaters-solver.gif │ ├── snippets-user-2.png │ ├── tota11y-button.png │ ├── triangular-grid.png │ ├── White-Lint-711890.jpg │ ├── johns-coordinates.png │ ├── mysterious-puzzle.jpg │ ├── slopefields │ │ ├── 4TcL.png │ │ ├── HB9P.png │ │ ├── JGuX.png │ │ ├── Mjew.png │ │ ├── YQtk.png │ │ ├── quV1.png │ │ └── sdKS.png │ ├── snippets-weekly-2.png │ ├── tota11y-expanded.png │ ├── tota11y-wikipedia.png │ ├── architects │ │ ├── c4sample.png │ │ └── signalboost.png │ ├── author-icons │ │ ├── carter.jpg │ │ ├── colin.png │ │ ├── dasani.jpg │ │ ├── diedra.jpg │ │ ├── hannah.jpg │ │ ├── jared.jpg │ │ ├── marta.jpg │ │ ├── ragini.jpg │ │ ├── samlau.jpg │ │ ├── ben-kraft.png │ │ ├── benalpert.png │ │ ├── beneater.png │ │ ├── benkamens.png │ │ ├── benkomalo.jpg │ │ ├── csilvers.png │ │ ├── davidwang.jpg │ │ ├── jamiewong.png │ │ ├── jangmi-jo.jpg │ │ ├── marcialee.png │ │ ├── nickbreen.jpg │ │ ├── rileyshaw.png │ │ ├── tomyedwab.png │ │ ├── wchargin.png │ │ ├── bryanclark.png │ │ ├── charliemarsh.jpg │ │ ├── chelseavoss.png │ │ ├── elifeasley.jpg │ │ ├── evykassirer.png │ │ ├── johnsullivan.png │ │ ├── jordanscales.png │ │ ├── joshcomeau.jpeg │ │ ├── kevindangoor.jpg │ │ ├── khanacademy.png │ │ ├── kimeriegreen.jpg │ │ ├── marcosojeda.jpg │ │ ├── scottgrant.png │ │ ├── shadajladdad.png │ │ ├── amos-latteier.jpg │ │ ├── brian-genisio.png │ │ ├── kevinbarabash.png │ │ ├── philliplemons.png │ │ ├── aasmundeldhuset.png │ │ ├── benjaminpollack.jpg │ │ └── olivernorthwood.jpg │ ├── a-really-small-app │ │ ├── apk.png │ │ └── app.gif │ ├── working-remotely │ │ ├── bread.jpg │ │ ├── cookie-cole.jpg │ │ ├── cookie-bouquet.png │ │ └── remote-work-baby-milo.png │ ├── auto-dumble │ │ ├── robot-wizard.png │ │ ├── sign-into-cb.png │ │ ├── cb-maintenance.png │ │ ├── slack-message-linking-disabled.png │ │ └── slack-message-linking-enabled.png │ ├── khanalytics │ │ ├── pipeline-logs.png │ │ └── completed-pipelines.png │ ├── lets-reduce │ │ ├── reduce-shapes.png │ │ └── visualization.gif │ ├── receiving-feedback │ │ ├── phab.jpg │ │ ├── shipit.jpg │ │ └── feedback.jpg │ ├── scaling-traffic-in-a-week.png │ ├── tota11y-github-contrast.png │ ├── tota11y-github-headings.png │ ├── healthy-hackathon │ │ ├── quidditch.jpg │ │ ├── hogwarts-bot.png │ │ ├── sorting-hat.jpg │ │ └── the-faculty.jpg │ ├── memcached-profiling │ │ ├── by-size.png │ │ └── static-cache.png │ ├── starting-android │ │ ├── core-tests.png │ │ └── first-android-commit.png │ ├── time-tracking │ │ ├── work-calendar.png │ │ ├── blank-calendar.png │ │ └── multiple-calendars.png │ ├── tota11y-wikipedia-headings.png │ ├── translation-server │ │ ├── science.jpg │ │ ├── graph_after.png │ │ ├── graph_before.png │ │ └── language_collage.png │ ├── dyslexic-friendly-fonts │ │ ├── after.png │ │ ├── before.png │ │ ├── settings.png │ │ └── font-comparision.png │ ├── introducing-swifttweaks │ │ ├── demo.gif │ │ └── overview.png │ ├── kotlin_adoption_instance_hours.png │ ├── python-refactor-3 │ │ ├── after_deps.png │ │ └── before_deps.png │ ├── stories-from-intern-class │ │ ├── meme.gif │ │ ├── wat.jpg │ │ ├── chelsea.jpg │ │ ├── group-pic.jpg │ │ └── screenshots.png │ ├── tips-for-code-reviews │ │ ├── ask-q-1.png │ │ ├── ask-q-2.png │ │ ├── comment-on-anything-1.png │ │ ├── comment-on-anything-2.png │ │ └── comment-on-anything-3.png │ ├── upgrade-buttons-links │ │ ├── image1.png │ │ ├── image2.png │ │ ├── image3.png │ │ ├── image4.png │ │ ├── image5.png │ │ └── image6.png │ ├── app-store-screenshots │ │ ├── app-store.png │ │ ├── sketch-file-preview.png │ │ └── fr_iPhoneSE_01_Explore.png │ ├── eng-principles-help-scale │ │ ├── khan-team.jpg │ │ └── alok-sharma-9wrFaeqMJBg-unsplash.jpg │ ├── engineering-principles │ │ └── fleet-grass.jpg │ ├── interning-at-khan-academy │ │ ├── image_0.png │ │ ├── image_1.png │ │ ├── image_2.png │ │ └── image_3.png │ ├── new-oss-activity │ │ └── react-multi-select.gif │ ├── no-cheating-allowed │ │ ├── Correct_Screen.png │ │ ├── HintClientArch.png │ │ └── TakingHint_Screen.png │ ├── switching-to-slack │ │ ├── testpages-slack.png │ │ ├── hipslack-enjoyment.png │ │ └── testpages-hipchat.png │ ├── windows-high-contrast-mode │ │ ├── lohp_whc.jpg │ │ ├── settings.jpg │ │ ├── comparison.jpg │ │ ├── logo_after.jpg │ │ ├── logo_before.jpg │ │ ├── lohp_default.jpg │ │ ├── modal_after.jpg │ │ ├── modal_before.jpg │ │ ├── timestamp_after.jpg │ │ ├── playbutton_after.jpg │ │ ├── playbutton_before.jpg │ │ └── timestamp_before.jpg │ ├── automating-translations │ │ ├── new-dashboard.png │ │ └── old-dashboard.png │ ├── prototyping-with-framer │ │ ├── framer-appicon.png │ │ └── example-app-layout.png │ ├── thumbnails │ │ ├── sample_sarcoplasmic_reticulum.png │ │ └── sample_interpreting_histograms.png │ ├── time-management-many-responses │ │ └── squirrel.jpg │ └── js-packaging-http2 │ │ ├── static-manual-waterfall-gae.png │ │ └── static-perfile-waterfall-gae.png ├── videos │ └── smart-translations.mp4 ├── supporting-files │ ├── README │ ├── generate_pickle_guards.py │ └── pickle_util.py ├── rss-template.xml ├── javascript │ └── katex-entry.js ├── posts │ ├── kotlin-for-python-developers.md │ ├── engineering-principles.md │ ├── handling-2x-traffic-in-a-week.md │ ├── no-cheating-allowed.rst │ ├── evil-puzzle.rst │ ├── new-oss-activity.md │ ├── eng-principles-help-scale.md │ ├── its-okay-to-break-things.md │ ├── introducing-swifttweaks.md │ ├── translation-server.md │ ├── khanalytics.md │ ├── tips-for-code-reviews.md │ ├── career-development.md │ ├── starting-android.md │ ├── snippet-server.md │ ├── tota11y.rst │ ├── architects-at-khan.md │ ├── original-serverless.md │ ├── react-native-monorepo.md │ ├── kanbanning-learnstorm-dev-process.md │ ├── using-static-analysis-in-Python-and-JavaScript-to-make-your-system-safer.md │ └── receiving-feedback.md ├── styles │ ├── common.css │ └── pygments.css ├── app.py ├── post.py └── gulpfile.js ├── .gitmodules ├── .arcconfig ├── README.rst ├── .arclint ├── .gitignore ├── phantomjs-tests ├── pages-are-responsive.js └── run-tests.sh └── LICENSE.rst /.mailmap: -------------------------------------------------------------------------------- 1 | Willow Chargin 2 | -------------------------------------------------------------------------------- /src/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/favicon.ico -------------------------------------------------------------------------------- /src/images/solver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/solver.gif -------------------------------------------------------------------------------- /src/images/evil-kitty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/evil-kitty.gif -------------------------------------------------------------------------------- /src/images/easy-solved.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/easy-solved.jpg -------------------------------------------------------------------------------- /src/images/tota11y-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tota11y-logo.png -------------------------------------------------------------------------------- /src/images/txn-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/txn-timeline.png -------------------------------------------------------------------------------- /src/images/txn-timeline.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/txn-timeline.xcf -------------------------------------------------------------------------------- /src/images/a11y-devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/a11y-devtools.png -------------------------------------------------------------------------------- /src/images/chameleon/cms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/chameleon/cms.png -------------------------------------------------------------------------------- /src/images/early-tota11y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/early-tota11y.png -------------------------------------------------------------------------------- /src/images/eaters-solver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/eaters-solver.gif -------------------------------------------------------------------------------- /src/images/snippets-user-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/snippets-user-2.png -------------------------------------------------------------------------------- /src/images/tota11y-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tota11y-button.png -------------------------------------------------------------------------------- /src/images/triangular-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/triangular-grid.png -------------------------------------------------------------------------------- /src/images/White-Lint-711890.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/White-Lint-711890.jpg -------------------------------------------------------------------------------- /src/images/johns-coordinates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/johns-coordinates.png -------------------------------------------------------------------------------- /src/images/mysterious-puzzle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/mysterious-puzzle.jpg -------------------------------------------------------------------------------- /src/images/slopefields/4TcL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/slopefields/4TcL.png -------------------------------------------------------------------------------- /src/images/slopefields/HB9P.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/slopefields/HB9P.png -------------------------------------------------------------------------------- /src/images/slopefields/JGuX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/slopefields/JGuX.png -------------------------------------------------------------------------------- /src/images/slopefields/Mjew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/slopefields/Mjew.png -------------------------------------------------------------------------------- /src/images/slopefields/YQtk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/slopefields/YQtk.png -------------------------------------------------------------------------------- /src/images/slopefields/quV1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/slopefields/quV1.png -------------------------------------------------------------------------------- /src/images/slopefields/sdKS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/slopefields/sdKS.png -------------------------------------------------------------------------------- /src/images/snippets-weekly-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/snippets-weekly-2.png -------------------------------------------------------------------------------- /src/images/tota11y-expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tota11y-expanded.png -------------------------------------------------------------------------------- /src/images/tota11y-wikipedia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tota11y-wikipedia.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "khan-linter"] 2 | path = khan-linter 3 | url = https://github.com/Khan/khan-linter.git 4 | -------------------------------------------------------------------------------- /src/images/architects/c4sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/architects/c4sample.png -------------------------------------------------------------------------------- /src/images/author-icons/carter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/carter.jpg -------------------------------------------------------------------------------- /src/images/author-icons/colin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/colin.png -------------------------------------------------------------------------------- /src/images/author-icons/dasani.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/dasani.jpg -------------------------------------------------------------------------------- /src/images/author-icons/diedra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/diedra.jpg -------------------------------------------------------------------------------- /src/images/author-icons/hannah.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/hannah.jpg -------------------------------------------------------------------------------- /src/images/author-icons/jared.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/jared.jpg -------------------------------------------------------------------------------- /src/images/author-icons/marta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/marta.jpg -------------------------------------------------------------------------------- /src/images/author-icons/ragini.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/ragini.jpg -------------------------------------------------------------------------------- /src/images/author-icons/samlau.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/samlau.jpg -------------------------------------------------------------------------------- /src/videos/smart-translations.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/videos/smart-translations.mp4 -------------------------------------------------------------------------------- /src/images/a-really-small-app/apk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/a-really-small-app/apk.png -------------------------------------------------------------------------------- /src/images/a-really-small-app/app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/a-really-small-app/app.gif -------------------------------------------------------------------------------- /src/images/architects/signalboost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/architects/signalboost.png -------------------------------------------------------------------------------- /src/images/author-icons/ben-kraft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/ben-kraft.png -------------------------------------------------------------------------------- /src/images/author-icons/benalpert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/benalpert.png -------------------------------------------------------------------------------- /src/images/author-icons/beneater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/beneater.png -------------------------------------------------------------------------------- /src/images/author-icons/benkamens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/benkamens.png -------------------------------------------------------------------------------- /src/images/author-icons/benkomalo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/benkomalo.jpg -------------------------------------------------------------------------------- /src/images/author-icons/csilvers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/csilvers.png -------------------------------------------------------------------------------- /src/images/author-icons/davidwang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/davidwang.jpg -------------------------------------------------------------------------------- /src/images/author-icons/jamiewong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/jamiewong.png -------------------------------------------------------------------------------- /src/images/author-icons/jangmi-jo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/jangmi-jo.jpg -------------------------------------------------------------------------------- /src/images/author-icons/marcialee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/marcialee.png -------------------------------------------------------------------------------- /src/images/author-icons/nickbreen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/nickbreen.jpg -------------------------------------------------------------------------------- /src/images/author-icons/rileyshaw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/rileyshaw.png -------------------------------------------------------------------------------- /src/images/author-icons/tomyedwab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/tomyedwab.png -------------------------------------------------------------------------------- /src/images/author-icons/wchargin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/wchargin.png -------------------------------------------------------------------------------- /src/images/working-remotely/bread.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/working-remotely/bread.jpg -------------------------------------------------------------------------------- /src/images/author-icons/bryanclark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/bryanclark.png -------------------------------------------------------------------------------- /src/images/author-icons/charliemarsh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/charliemarsh.jpg -------------------------------------------------------------------------------- /src/images/author-icons/chelseavoss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/chelseavoss.png -------------------------------------------------------------------------------- /src/images/author-icons/elifeasley.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/elifeasley.jpg -------------------------------------------------------------------------------- /src/images/author-icons/evykassirer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/evykassirer.png -------------------------------------------------------------------------------- /src/images/author-icons/johnsullivan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/johnsullivan.png -------------------------------------------------------------------------------- /src/images/author-icons/jordanscales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/jordanscales.png -------------------------------------------------------------------------------- /src/images/author-icons/joshcomeau.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/joshcomeau.jpeg -------------------------------------------------------------------------------- /src/images/author-icons/kevindangoor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/kevindangoor.jpg -------------------------------------------------------------------------------- /src/images/author-icons/khanacademy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/khanacademy.png -------------------------------------------------------------------------------- /src/images/author-icons/kimeriegreen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/kimeriegreen.jpg -------------------------------------------------------------------------------- /src/images/author-icons/marcosojeda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/marcosojeda.jpg -------------------------------------------------------------------------------- /src/images/author-icons/scottgrant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/scottgrant.png -------------------------------------------------------------------------------- /src/images/author-icons/shadajladdad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/shadajladdad.png -------------------------------------------------------------------------------- /src/images/auto-dumble/robot-wizard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/auto-dumble/robot-wizard.png -------------------------------------------------------------------------------- /src/images/auto-dumble/sign-into-cb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/auto-dumble/sign-into-cb.png -------------------------------------------------------------------------------- /src/images/chameleon/scratchpad-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/chameleon/scratchpad-demo.gif -------------------------------------------------------------------------------- /src/images/khanalytics/pipeline-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/khanalytics/pipeline-logs.png -------------------------------------------------------------------------------- /src/images/lets-reduce/reduce-shapes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/lets-reduce/reduce-shapes.png -------------------------------------------------------------------------------- /src/images/lets-reduce/visualization.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/lets-reduce/visualization.gif -------------------------------------------------------------------------------- /src/images/receiving-feedback/phab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/receiving-feedback/phab.jpg -------------------------------------------------------------------------------- /src/images/receiving-feedback/shipit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/receiving-feedback/shipit.jpg -------------------------------------------------------------------------------- /src/images/scaling-traffic-in-a-week.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/scaling-traffic-in-a-week.png -------------------------------------------------------------------------------- /src/images/tota11y-github-contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tota11y-github-contrast.png -------------------------------------------------------------------------------- /src/images/tota11y-github-headings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tota11y-github-headings.png -------------------------------------------------------------------------------- /src/images/author-icons/amos-latteier.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/amos-latteier.jpg -------------------------------------------------------------------------------- /src/images/author-icons/brian-genisio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/brian-genisio.png -------------------------------------------------------------------------------- /src/images/author-icons/kevinbarabash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/kevinbarabash.png -------------------------------------------------------------------------------- /src/images/author-icons/philliplemons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/philliplemons.png -------------------------------------------------------------------------------- /src/images/auto-dumble/cb-maintenance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/auto-dumble/cb-maintenance.png -------------------------------------------------------------------------------- /src/images/healthy-hackathon/quidditch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/healthy-hackathon/quidditch.jpg -------------------------------------------------------------------------------- /src/images/memcached-profiling/by-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/memcached-profiling/by-size.png -------------------------------------------------------------------------------- /src/images/receiving-feedback/feedback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/receiving-feedback/feedback.jpg -------------------------------------------------------------------------------- /src/images/starting-android/core-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/starting-android/core-tests.png -------------------------------------------------------------------------------- /src/images/time-tracking/work-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/time-tracking/work-calendar.png -------------------------------------------------------------------------------- /src/images/tota11y-wikipedia-headings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tota11y-wikipedia-headings.png -------------------------------------------------------------------------------- /src/images/translation-server/science.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/translation-server/science.jpg -------------------------------------------------------------------------------- /src/images/author-icons/aasmundeldhuset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/aasmundeldhuset.png -------------------------------------------------------------------------------- /src/images/author-icons/benjaminpollack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/benjaminpollack.jpg -------------------------------------------------------------------------------- /src/images/author-icons/olivernorthwood.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/author-icons/olivernorthwood.jpg -------------------------------------------------------------------------------- /src/images/dyslexic-friendly-fonts/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/dyslexic-friendly-fonts/after.png -------------------------------------------------------------------------------- /src/images/dyslexic-friendly-fonts/before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/dyslexic-friendly-fonts/before.png -------------------------------------------------------------------------------- /src/images/healthy-hackathon/hogwarts-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/healthy-hackathon/hogwarts-bot.png -------------------------------------------------------------------------------- /src/images/healthy-hackathon/sorting-hat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/healthy-hackathon/sorting-hat.jpg -------------------------------------------------------------------------------- /src/images/healthy-hackathon/the-faculty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/healthy-hackathon/the-faculty.jpg -------------------------------------------------------------------------------- /src/images/introducing-swifttweaks/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/introducing-swifttweaks/demo.gif -------------------------------------------------------------------------------- /src/images/kotlin_adoption_instance_hours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/kotlin_adoption_instance_hours.png -------------------------------------------------------------------------------- /src/images/python-refactor-3/after_deps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/python-refactor-3/after_deps.png -------------------------------------------------------------------------------- /src/images/python-refactor-3/before_deps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/python-refactor-3/before_deps.png -------------------------------------------------------------------------------- /src/images/stories-from-intern-class/meme.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/stories-from-intern-class/meme.gif -------------------------------------------------------------------------------- /src/images/stories-from-intern-class/wat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/stories-from-intern-class/wat.jpg -------------------------------------------------------------------------------- /src/images/time-tracking/blank-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/time-tracking/blank-calendar.png -------------------------------------------------------------------------------- /src/images/tips-for-code-reviews/ask-q-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tips-for-code-reviews/ask-q-1.png -------------------------------------------------------------------------------- /src/images/tips-for-code-reviews/ask-q-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tips-for-code-reviews/ask-q-2.png -------------------------------------------------------------------------------- /src/images/translation-server/graph_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/translation-server/graph_after.png -------------------------------------------------------------------------------- /src/images/upgrade-buttons-links/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/upgrade-buttons-links/image1.png -------------------------------------------------------------------------------- /src/images/upgrade-buttons-links/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/upgrade-buttons-links/image2.png -------------------------------------------------------------------------------- /src/images/upgrade-buttons-links/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/upgrade-buttons-links/image3.png -------------------------------------------------------------------------------- /src/images/upgrade-buttons-links/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/upgrade-buttons-links/image4.png -------------------------------------------------------------------------------- /src/images/upgrade-buttons-links/image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/upgrade-buttons-links/image5.png -------------------------------------------------------------------------------- /src/images/upgrade-buttons-links/image6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/upgrade-buttons-links/image6.png -------------------------------------------------------------------------------- /src/images/working-remotely/cookie-cole.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/working-remotely/cookie-cole.jpg -------------------------------------------------------------------------------- /src/images/app-store-screenshots/app-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/app-store-screenshots/app-store.png -------------------------------------------------------------------------------- /src/images/dyslexic-friendly-fonts/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/dyslexic-friendly-fonts/settings.png -------------------------------------------------------------------------------- /src/images/introducing-swifttweaks/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/introducing-swifttweaks/overview.png -------------------------------------------------------------------------------- /src/images/khanalytics/completed-pipelines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/khanalytics/completed-pipelines.png -------------------------------------------------------------------------------- /src/images/memcached-profiling/static-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/memcached-profiling/static-cache.png -------------------------------------------------------------------------------- /src/images/time-tracking/multiple-calendars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/time-tracking/multiple-calendars.png -------------------------------------------------------------------------------- /src/images/translation-server/graph_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/translation-server/graph_before.png -------------------------------------------------------------------------------- /src/images/working-remotely/cookie-bouquet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/working-remotely/cookie-bouquet.png -------------------------------------------------------------------------------- /src/images/eng-principles-help-scale/khan-team.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/eng-principles-help-scale/khan-team.jpg -------------------------------------------------------------------------------- /src/images/engineering-principles/fleet-grass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/engineering-principles/fleet-grass.jpg -------------------------------------------------------------------------------- /src/images/interning-at-khan-academy/image_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/interning-at-khan-academy/image_0.png -------------------------------------------------------------------------------- /src/images/interning-at-khan-academy/image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/interning-at-khan-academy/image_1.png -------------------------------------------------------------------------------- /src/images/interning-at-khan-academy/image_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/interning-at-khan-academy/image_2.png -------------------------------------------------------------------------------- /src/images/interning-at-khan-academy/image_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/interning-at-khan-academy/image_3.png -------------------------------------------------------------------------------- /src/images/new-oss-activity/react-multi-select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/new-oss-activity/react-multi-select.gif -------------------------------------------------------------------------------- /src/images/no-cheating-allowed/Correct_Screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/no-cheating-allowed/Correct_Screen.png -------------------------------------------------------------------------------- /src/images/no-cheating-allowed/HintClientArch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/no-cheating-allowed/HintClientArch.png -------------------------------------------------------------------------------- /src/images/stories-from-intern-class/chelsea.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/stories-from-intern-class/chelsea.jpg -------------------------------------------------------------------------------- /src/images/stories-from-intern-class/group-pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/stories-from-intern-class/group-pic.jpg -------------------------------------------------------------------------------- /src/images/switching-to-slack/testpages-slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/switching-to-slack/testpages-slack.png -------------------------------------------------------------------------------- /src/images/translation-server/language_collage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/translation-server/language_collage.png -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/lohp_whc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/lohp_whc.jpg -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/settings.jpg -------------------------------------------------------------------------------- /src/images/automating-translations/new-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/automating-translations/new-dashboard.png -------------------------------------------------------------------------------- /src/images/automating-translations/old-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/automating-translations/old-dashboard.png -------------------------------------------------------------------------------- /src/images/no-cheating-allowed/TakingHint_Screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/no-cheating-allowed/TakingHint_Screen.png -------------------------------------------------------------------------------- /src/images/starting-android/first-android-commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/starting-android/first-android-commit.png -------------------------------------------------------------------------------- /src/images/stories-from-intern-class/screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/stories-from-intern-class/screenshots.png -------------------------------------------------------------------------------- /src/images/switching-to-slack/hipslack-enjoyment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/switching-to-slack/hipslack-enjoyment.png -------------------------------------------------------------------------------- /src/images/switching-to-slack/testpages-hipchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/switching-to-slack/testpages-hipchat.png -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/comparison.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/comparison.jpg -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/logo_after.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/logo_after.jpg -------------------------------------------------------------------------------- /src/images/chameleon/partially-translated-exercise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/chameleon/partially-translated-exercise.png -------------------------------------------------------------------------------- /src/images/dyslexic-friendly-fonts/font-comparision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/dyslexic-friendly-fonts/font-comparision.png -------------------------------------------------------------------------------- /src/images/prototyping-with-framer/framer-appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/prototyping-with-framer/framer-appicon.png -------------------------------------------------------------------------------- /src/images/thumbnails/sample_sarcoplasmic_reticulum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/thumbnails/sample_sarcoplasmic_reticulum.png -------------------------------------------------------------------------------- /src/images/time-management-many-responses/squirrel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/time-management-many-responses/squirrel.jpg -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/logo_before.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/logo_before.jpg -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/lohp_default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/lohp_default.jpg -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/modal_after.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/modal_after.jpg -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/modal_before.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/modal_before.jpg -------------------------------------------------------------------------------- /src/images/working-remotely/remote-work-baby-milo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/working-remotely/remote-work-baby-milo.png -------------------------------------------------------------------------------- /src/images/app-store-screenshots/sketch-file-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/app-store-screenshots/sketch-file-preview.png -------------------------------------------------------------------------------- /src/images/auto-dumble/slack-message-linking-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/auto-dumble/slack-message-linking-disabled.png -------------------------------------------------------------------------------- /src/images/auto-dumble/slack-message-linking-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/auto-dumble/slack-message-linking-enabled.png -------------------------------------------------------------------------------- /src/images/prototyping-with-framer/example-app-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/prototyping-with-framer/example-app-layout.png -------------------------------------------------------------------------------- /src/images/thumbnails/sample_interpreting_histograms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/thumbnails/sample_interpreting_histograms.png -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/timestamp_after.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/timestamp_after.jpg -------------------------------------------------------------------------------- /src/images/app-store-screenshots/fr_iPhoneSE_01_Explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/app-store-screenshots/fr_iPhoneSE_01_Explore.png -------------------------------------------------------------------------------- /src/images/tips-for-code-reviews/comment-on-anything-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tips-for-code-reviews/comment-on-anything-1.png -------------------------------------------------------------------------------- /src/images/tips-for-code-reviews/comment-on-anything-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tips-for-code-reviews/comment-on-anything-2.png -------------------------------------------------------------------------------- /src/images/tips-for-code-reviews/comment-on-anything-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/tips-for-code-reviews/comment-on-anything-3.png -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/playbutton_after.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/playbutton_after.jpg -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/playbutton_before.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/playbutton_before.jpg -------------------------------------------------------------------------------- /src/images/windows-high-contrast-mode/timestamp_before.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/windows-high-contrast-mode/timestamp_before.jpg -------------------------------------------------------------------------------- /src/images/js-packaging-http2/static-manual-waterfall-gae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/js-packaging-http2/static-manual-waterfall-gae.png -------------------------------------------------------------------------------- /src/images/js-packaging-http2/static-perfile-waterfall-gae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/js-packaging-http2/static-perfile-waterfall-gae.png -------------------------------------------------------------------------------- /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "project_id": "engblog", 3 | "conduit_uri": "https://phabricator.khanacademy.org/", 4 | "lint.engine": "ArcanistConfigurationDrivenLintEngine" 5 | } 6 | -------------------------------------------------------------------------------- /src/images/eng-principles-help-scale/alok-sharma-9wrFaeqMJBg-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Khan/engblog/HEAD/src/images/eng-principles-help-scale/alok-sharma-9wrFaeqMJBg-unsplash.jpg -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Deprecated! 2 | 3 | This blog is now located at https://blog.khanacademy.org/engineering/ 4 | 5 | If you're a Khan Academy employee and want to contribute to the blog, 6 | go to https://www.khanacademy.org/r/engblog. -------------------------------------------------------------------------------- /src/supporting-files/README: -------------------------------------------------------------------------------- 1 | This directory holds files that you want to link to or include in your 2 | blog post, that are not images. (You could just put such files in 3 | images/ instead, but that's a pretty misleading name in that case!) 4 | -------------------------------------------------------------------------------- /.arclint: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "khan-linter": { 4 | "type": "script-and-regex", 5 | "script-and-regex.script": "ka-lint --always-exit-0 --blacklist=yes --propose-arc-fixes", 6 | "script-and-regex.regex": "\/^((?P[^:]*):(?P\\d+):((?P\\d+):)? (?P((?PE)|(?PW))\\S+) (?P[^\\x00]*)(\\x00(?P[^\\x00]*)\\x00(?P[^\\x00]*)\\x00)?)|(?PSKIPPING.*)$\/m" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/rss-template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | KA Engineering 5 | http://engineering.khanacademy.org 6 | Khan Academy Engineering's blog 7 | {{#posts}} 8 | 9 | {{title}} 10 | http://engineering.khanacademy.org/{{relative_href}} 11 | {{date}} 12 | http://engineering.khanacademy.org/{{relative_href}} 13 | 14 | {{/posts}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | #Translations 30 | *.mo 31 | 32 | # Editors 33 | .vscode 34 | 35 | #Mr Developer 36 | .mr.developer.cfg 37 | 38 | .backup 39 | 40 | # Credentials cache 41 | .cache/ 42 | 43 | # Sphinx 44 | docs/_build/ 45 | 46 | # Vim files 47 | *~ 48 | 49 | # Super zips 50 | *.sz 51 | 52 | # Virtual Environments 53 | venv*/ 54 | env/ 55 | 56 | # JS! 57 | bower_components/ 58 | node_modules/ 59 | 60 | output/ 61 | -------------------------------------------------------------------------------- /src/javascript/katex-entry.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Entry point to auto-render KaTeX in the article once loaded. 3 | */ 4 | (function() { 5 | var id = setInterval(function() { 6 | if (!(window.katex && window.renderMathInElement)) { 7 | return; 8 | } 9 | window.clearInterval(id); 10 | var article = document.getElementsByTagName('article')[0]; 11 | 12 | // We add single-dollar delimiters because escaping Markdown is annoying. 13 | // Note that $$ must come before $ for the former to ever be parsed. 14 | var delimiters = [ 15 | {left: "$$", right: "$$", display: true}, 16 | {left: "$", right: "$", display: false}, // must come after $$ 17 | {left: "\\[", right: "\\]", display: true}, 18 | {left: "\\(", right: "\\)", display: false} 19 | ]; 20 | renderMathInElement(article, { delimiters: delimiters }); 21 | }, 100); 22 | })(); 23 | -------------------------------------------------------------------------------- /phantomjs-tests/pages-are-responsive.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var, no-console */ /* because we are using es5 */ 2 | /** 3 | * Test that a page resizes to perfectly fit the width of the viewport. 4 | * 5 | * The script will return with an exit status of 0 on success, and 1 on 6 | * failure. 7 | */ 8 | 9 | var system = require("system"); 10 | var webpage = require("webpage"); 11 | 12 | if (system.args.length !== 3) { 13 | console.log("USAGE:", system.args[0], " [URL] [VIEWPORT_WIDTH]"); 14 | phantom.exit(1); 15 | } 16 | var url = system.args[1]; 17 | var viewportWidth = parseInt(system.args[2]); 18 | 19 | var page = webpage.create(); 20 | page.viewportSize = {width: viewportWidth, height: 320}; 21 | page.onLoadFinished = function(status) { 22 | if (status !== "success") { 23 | console.log("\tResponsiveness test failed. Failed to fetch", url); 24 | phantom.exit(1); 25 | return; 26 | } 27 | 28 | var width = page.evaluate(function() { 29 | // scrollWidth takes into account any oversized content, which is 30 | // exactly what we're looking for. 31 | return document.body.scrollWidth; 32 | }); 33 | 34 | var testPassed = width === viewportWidth; 35 | if (!testPassed) { 36 | console.log("\tResponsiveness test failed. Expected width", 37 | viewportWidth, "got", width); 38 | } 39 | 40 | phantom.exit(width === viewportWidth ? 0 : 1); 41 | }; 42 | page.open(url); 43 | -------------------------------------------------------------------------------- /src/posts/kotlin-for-python-developers.md: -------------------------------------------------------------------------------- 1 | title: Kotlin for Python developers 2 | published_on: November 29, 2018 3 | author: Aasmund Eldhuset 4 | team: Content Platform 5 | ... 6 | 7 | Earlier this year, the Khan Academy Engineering organization decided to [introduce Kotlin as an alternative backend language](/posts/kotlin-adoption.htm) alongside Python (in which almost all of our existing backend codebase is written) — see the linked post for details on our decision process. While there are a number of excellent resources for learning Kotlin, most of them seem geared towards people who already know Java. So I decided to write a quick "getting started" guide that would explain Kotlin in terms of Python. It ballooned a bit in scope and thoroughness as I wrote it, and ended up being fairly extensive. 8 | 9 | In the hopes that other developers will find it useful, we have released the resulting [**Kotlin for Python developers**](https://khan.github.io/kotlin-for-python-developers/) document publicly! It should also be useful to developers who are used to other dynamically typed languages. 10 | 11 | Please note that it is not intended to cover all the features of Kotlin, and that it is written in the form of a textbook rather than as a reference manual. It is not a part of Khan Academy's official product offering, but rather an internal resource that we're providing "as is" for the benefit of the programming community, so the quality bar is lower than for our official educational content — if you find any errors, please submit an [issue](https://github.com/Khan/kotlin-for-python-developers/issues) or a [pull request](https://github.com/Khan/kotlin-for-python-developers/pulls). 12 | -------------------------------------------------------------------------------- /phantomjs-tests/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Note that $(cd x; pwd) is simlar to $(realpath x) 4 | TEST_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | ROOT_DIR="$(cd "$TEST_DIR/.."; pwd)" 6 | OUTPUT_DIR="$(cd "$ROOT_DIR/output"; pwd)" 7 | 8 | "$ROOT_DIR/node_modules/http-server/bin/http-server" \ 9 | "$OUTPUT_DIR" -s -p 9104 -a 127.0.0.1 -d false -e htm & 10 | SERVER_PID=$! 11 | 12 | # Wait until the server starts up 13 | until $(curl --head --fail http://127.0.0.1:9104 > /dev/null 2>&1); do 14 | sleep 1 15 | done 16 | 17 | # Grab a list of all the URLs (we do this by scraping the RSS feed) 18 | # TODO(johnsullivan): make a sitemap and then scrape that 19 | LINK_RE='http://engineering.khanacademy.org\(.*\)' 20 | REPLACEMENT='http://127.0.0.1:9104\1' 21 | URLS=$( 22 | curl http://127.0.0.1:9104/rss.xml 2> /dev/null | 23 | sed -n "s#$LINK_RE#$REPLACEMENT#p" | 24 | xargs echo 25 | ) 26 | 27 | # A responsive test failure on any of these pages will be ignored 28 | RESPONSIVE_TEST_WHITELIST=" 29 | # This fails because some tables are too wide. Tricky to fix, not sure how 30 | # to reformat the data to be thinner. 31 | http://127.0.0.1:9104/posts/js-packaging-http2.htm 32 | 33 | # TODO(johnsullivan): I don't know why this fails yet... And can't 34 | # reproduce it locally. 35 | http://127.0.0.1:9104/posts/i18nize-templates.htm 36 | 37 | # This test fails intermittently (if the JS renders quickly enough it will 38 | # fail because of its long equations). 39 | http://127.0.0.1:9104/posts/making-thumbnails-fast.htm 40 | 41 | # NOTE (josh): Fails because of a max-width issue, although everything 42 | # looks alright to me at 320px wide... 43 | http://127.0.0.1:9104/posts/lets-reduce.htm 44 | " 45 | 46 | # By default, return a successful exit code 47 | EXIT_CODE=0 48 | 49 | for URL in $URLS; do 50 | echo "[$URL]" 51 | if ! phantomjs "$TEST_DIR/pages-are-responsive.js" "$URL" 320; then 52 | if grep -q "$URL" <<< "$RESPONSIVE_TEST_WHITELIST"; then 53 | echo -e "\tIgnoring responsive test failure because of whitelist." 54 | else 55 | EXIT_CODE=1 56 | fi 57 | fi 58 | done 59 | 60 | kill $SERVER_PID 61 | exit "$EXIT_CODE" 62 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Licensing Information 2 | ===================== 3 | 4 | Simple Summary 5 | ---------------- 6 | 7 | The blog's code is available under the MIT license, posts are available under the CC BY 4.0 license, and submodules have separate licensing in them (but are all OSS too). 8 | 9 | The Full Story 10 | -------------- 11 | 12 | All code in this repository (**except for** code in submodules, code that is embedded in blog posts, code describing the posts in ``src/posts/``, and images for those posts in ``src/images/``) is licensed per the terms of The MIT License included below. 13 | 14 | All posts in this repository in ``src/posts/`` and images for those posts in ``src/images/`` are licensed per the terms of the Creative Commons Attribution 4.0 International available at `creativecommons.org/licenses/by/4.0/ `_. 15 | 16 | Use of the Khan Academy brand has `separate restrictions under trademark law `_, that are not affected by this licensing. So please don't create a site that seems like it's affiliated with Khan Academy. 17 | 18 | The MIT License 19 | --------------- 20 | 21 | Copyright (c) 2015 Khan Academy 22 | 23 | Permission is hereby granted, free of charge, to any person obtaining a copy 24 | of this software and associated documentation files (the "Software"), to deal 25 | in the Software without restriction, including without limitation the rights 26 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 27 | copies of the Software, and to permit persons to whom the Software is 28 | furnished to do so, subject to the following conditions: 29 | 30 | The above copyright notice and this permission notice shall be included in 31 | all copies or substantial portions of the Software. 32 | 33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 34 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 35 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 36 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 37 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 38 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 39 | THE SOFTWARE. 40 | -------------------------------------------------------------------------------- /src/styles/common.css: -------------------------------------------------------------------------------- 1 | /* The text banners here are useful for users of Sublime Text. They were 2 | * generated with the Roman font at http://www.network-science.de/ascii/ */ 3 | 4 | body { 5 | font-family: 'Lato', 'Helvetica Neue', 'Arial', sans-serif; 6 | src: url('https://fonts.googleapis.com/css?family=Lato'); 7 | 8 | /* True white is often too bright to use as a background */ 9 | background-color: #F7F8FA; 10 | 11 | /* True black is often too dark to use as a base font color */ 12 | color: #21242C; 13 | 14 | /* This will keep any text from riding up too close to any of the 15 | * edges of the page. */ 16 | padding-bottom: 5em; 17 | margin: 0; 18 | 19 | font-size: 16px; 20 | } 21 | 22 | .bounded { 23 | padding: 0 2em; 24 | } 25 | 26 | @media (min-width: 1500px) { 27 | body { font-size: 19px; } 28 | } 29 | 30 | @media (max-width: 1000px) { 31 | body { font-size: 15px; } 32 | } 33 | 34 | @media (max-width: 300px) { 35 | body { font-size: 11px; } 36 | } 37 | 38 | strong, b { 39 | /* I prefer semi-bold to a full bold */ 40 | font-weight: 600; 41 | } 42 | 43 | a { 44 | /* Our links will be Wonder Blocks blue and not have underlines */ 45 | text-decoration: none; 46 | color: #1865F2; 47 | } 48 | 49 | a:hover { 50 | /* When the user hovers over a link show an underline in case they were 51 | * unsure if it was really a link (we do this mostly because it looks 52 | * cool but it's fun to pretend). */ 53 | text-decoration: underline; 54 | } 55 | 56 | /* We mention #content here because we need to raise the specificity of this 57 | * selector to override the pure.css selector. */ 58 | #content .pure-form legend, #content .pure-form fieldset { 59 | /* By default, legend and fieldset elements have a border on the bottom. It 60 | * doesn't look very good so we want to get rid of it. */ 61 | border-bottom: none; 62 | } 63 | 64 | a.pure-button-primary, button.pure-button-primary { 65 | /* The default color that Pure.css uses is a little too harsh imo 66 | * so this is a much more faded blue. */ 67 | background-color: #5CADB5; 68 | 69 | /* The white on blue can be difficult to read if it's not bolded, 70 | * this also makes the primary button stand out more. */ 71 | font-weight: 600; 72 | } 73 | 74 | .faded { 75 | opacity: .7; 76 | } 77 | 78 | .align-center { 79 | display: block; 80 | margin: 0 auto; 81 | } 82 | -------------------------------------------------------------------------------- /src/styles/pygments.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | .c { color: #999988; font-style: italic } /* Comment */ 3 | .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | .k { font-weight: bold } /* Keyword */ 5 | .o { font-weight: bold } /* Operator */ 6 | .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 8 | .c1 { color: #999988; font-style: italic } /* Comment.Single */ 9 | .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 10 | .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 11 | .ge { font-style: italic } /* Generic.Emph */ 12 | .gr { color: #aa0000 } /* Generic.Error */ 13 | .gh { color: #999999 } /* Generic.Heading */ 14 | .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 15 | .go { color: #888888 } /* Generic.Output */ 16 | .gp { color: #555555 } /* Generic.Prompt */ 17 | .gs { font-weight: bold } /* Generic.Strong */ 18 | .gu { color: #aaaaaa } /* Generic.Subheading */ 19 | .gt { color: #aa0000 } /* Generic.Traceback */ 20 | .kc { font-weight: bold } /* Keyword.Constant */ 21 | .kd { font-weight: bold } /* Keyword.Declaration */ 22 | .kn { font-weight: bold } /* Keyword.Namespace */ 23 | .kp { font-weight: bold } /* Keyword.Pseudo */ 24 | .kr { font-weight: bold } /* Keyword.Reserved */ 25 | .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 26 | .m { color: #009999 } /* Literal.Number */ 27 | .s { color: #bb8844 } /* Literal.String */ 28 | .na { color: #008080 } /* Name.Attribute */ 29 | .nb { color: #999999 } /* Name.Builtin */ 30 | .nc { color: #445588; font-weight: bold } /* Name.Class */ 31 | .no { color: #008080 } /* Name.Constant */ 32 | .ni { color: #800080 } /* Name.Entity */ 33 | .ne { color: #990000; font-weight: bold } /* Name.Exception */ 34 | .nf { color: #990000; font-weight: bold } /* Name.Function */ 35 | .nn { color: #555555 } /* Name.Namespace */ 36 | .nt { color: #000080 } /* Name.Tag */ 37 | .nv { color: #008080 } /* Name.Variable */ 38 | .ow { font-weight: bold } /* Operator.Word */ 39 | .w { color: #bbbbbb } /* Text.Whitespace */ 40 | .mf { color: #009999 } /* Literal.Number.Float */ 41 | .mh { color: #009999 } /* Literal.Number.Hex */ 42 | .mi { color: #009999 } /* Literal.Number.Integer */ 43 | .mo { color: #009999 } /* Literal.Number.Oct */ 44 | .sb { color: #bb8844 } /* Literal.String.Backtick */ 45 | .sc { color: #bb8844 } /* Literal.String.Char */ 46 | .sd { color: #bb8844 } /* Literal.String.Doc */ 47 | .s2 { color: #bb8844 } /* Literal.String.Double */ 48 | .se { color: #bb8844 } /* Literal.String.Escape */ 49 | .sh { color: #bb8844 } /* Literal.String.Heredoc */ 50 | .si { color: #bb8844 } /* Literal.String.Interpol */ 51 | .sx { color: #bb8844 } /* Literal.String.Other */ 52 | .sr { color: #808000 } /* Literal.String.Regex */ 53 | .s1 { color: #bb8844 } /* Literal.String.Single */ 54 | .ss { color: #bb8844 } /* Literal.String.Symbol */ 55 | .bp { color: #999999 } /* Name.Builtin.Pseudo */ 56 | .vc { color: #008080 } /* Name.Variable.Class */ 57 | .vg { color: #008080 } /* Name.Variable.Global */ 58 | .vi { color: #008080 } /* Name.Variable.Instance */ 59 | .il { color: #009999 } /* Literal.Number.Integer.Long */ 60 | -------------------------------------------------------------------------------- /src/posts/engineering-principles.md: -------------------------------------------------------------------------------- 1 | title: "Khan Academy's Engineering Principles" 2 | published_on: June 6, 2016 3 | author: Ben Kamens 4 | team: Eng Leads 5 | ... 6 | 7 | You know those super-frustrating movie scenes where the entire plotline is driven by some sort of completely avoidable communication failure? 8 | 9 | Where if the two characters WOULD JUST FRIGGIN' TALK TO EACH OTHER, the whole shebang could've been avoided? And you, you poor shmuck in the audience, just have to sit there and watch the author jump through all sorts of hoops to justify why these two otherwise-humanesque characters seem to be woefully incapable of talking like humans? 10 | 11 | [The "communication failure" trope](http://tvtropes.org/pmwiki/pmwiki.php/Main/PoorCommunicationKills) seems super contrived when I'm watching a movie. But when playing my role as part of a growing company, I'm part of an intricate system that's constantly creating communication failures just as epic and just as avoidable. 12 | 13 | [Growing, changing companies are breeding grounds for communication failures](https://twitter.com/aunder/status/704408612588933120), no matter how thoughtful and brilliant and well-spoken every single employee is. Sad! 14 | 15 | One of the ways to innoculate your company against such a painful reality is by building a team that doesn't require perfect communication to succeed. 16 | 17 | ![Fleetwood communicates well](/images/engineering-principles/fleet-grass.jpg) 18 | 19 | That's how a lot of "company values" documents get started: with a desire to give every single person the context they need such that without perfect communication, one can make decisions in stride with the rest. By establishing a few key cultural messages that can be repeated — as new hires join, as processes change, as teams rearrange, as priorities shift — you can create a team that gets stronger while communication keeps getting harder. 20 | 21 | # Our principles, shared with you 22 | 23 | Our engineering team is growing, communication is getting harder, and it's important that every single person wields a few key principles that can be used to make decisions in the face of ambiguity. 24 | 25 | So we wrote ours down and are now sharing 'em with you. You're free to use this doc however you want — remix it, disagree with it, print it out and fold it into boat. 26 | 27 | ##
[⇢ Khan Academy Engineering Principles ⇠](https://docs.google.com/document/d/1PW4NYn9pYNam2EuGEsTN9pTgwTfFnT_R9OZLJJICWQU/edit)
28 | 29 | In writing this we were inspired by [Gusto's version](http://engineering.gusto.com/our-engineering-values-and-principles/). If you read 'em, notice that they're meaningful [_in that they take guts and inflict pain_](https://hbr.org/2002/07/make-your-values-mean-something). Gusto's "**we're here to help**" value is particularly gutsy in today's Silicon Valley culture. 30 | 31 | It's certainly taken guts to stick with Khan Academy's "**shipping beats perfection**" and "**anybody can fix anything**" values. It's also been hugely valuable. 32 | 33 | I'm proud whenever I see our principles help somebody move forward in the face of ambiguity. That's exactly the point — empower people to get going without requiring perfect communication. We're gonna need more and more of that strength as we grow. 34 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | """A script that builds the blog's content. 2 | 3 | Run this with `python app.py output_dir`. 4 | """ 5 | 6 | import os 7 | import sys 8 | import glob 9 | 10 | import pystache 11 | 12 | import info 13 | from post import Post 14 | 15 | POST_TEMPLATE = open("post-template.htm", "rb").read().decode("utf-8") 16 | RSS_TEMPLATE = open("rss-template.xml", "rb").read().decode("utf-8") 17 | 18 | 19 | def render_post_page(posts, current_post_index): 20 | """Renders a single post page. 21 | 22 | Arguments: 23 | posts - A list of Post objects sorted by published date. 24 | current_post_index - The index of the current post in the list of 25 | posts. 26 | 27 | Returns: A string or unicode object containing the rendered page. 28 | """ 29 | def get_post_dict(index): 30 | if 0 <= index < len(posts): 31 | return posts[index].to_dict() 32 | 33 | return None 34 | 35 | template_params = { 36 | "posts": [post.to_dict() for post in posts], 37 | "displayed_post": get_post_dict(current_post_index), 38 | "html_content": posts[current_post_index].get_html_content(), 39 | "next_post": get_post_dict(current_post_index - 1), 40 | "previous_post": get_post_dict(current_post_index + 1), 41 | "upcoming_post": info.upcoming_post, 42 | } 43 | renderer = pystache.Renderer(missing_tags="strict") 44 | return renderer.render(POST_TEMPLATE, template_params) 45 | 46 | 47 | def render_rss_page(posts): 48 | """Renders the RSS feed. 49 | 50 | Arguments: 51 | posts - A list of Post objects sorted by published date. 52 | 53 | Returns: A string or unicode object containing the rendered page. 54 | """ 55 | to_rss_string = lambda d: d.strftime("%a, %d %b %Y 11:00:00 GMT-8") 56 | 57 | template_params = { 58 | "posts": [ 59 | { 60 | "title": post.title, 61 | "relative_href": "posts/" + post.get_output_name(), 62 | "date": to_rss_string(post.published_on), 63 | } 64 | for post in posts 65 | ] 66 | } 67 | renderer = pystache.Renderer(missing_tags="strict") 68 | return renderer.render(RSS_TEMPLATE, template_params) 69 | 70 | 71 | def main(output_directory): 72 | # Grab all of the posts and sort them by their published date 73 | posts = [Post(path) for path in glob.glob("posts/*")] 74 | posts = sorted(posts, reverse=True, key=lambda post: post.published_on) 75 | 76 | # Make a posts directory in our output directory 77 | os.mkdir(os.path.join(output_directory, "posts")) 78 | 79 | # Go through and create all the post pages 80 | for index, post in enumerate(posts): 81 | output_path = os.path.join(output_directory, "posts", 82 | post.get_output_name()) 83 | rendered_post = render_post_page(posts, index) 84 | 85 | with open(output_path, "wb") as f: 86 | f.write(rendered_post.encode("utf-8")) 87 | 88 | # If this is the current post, we also make it the index page 89 | if index == 0: 90 | with open(os.path.join(output_directory, "index.htm"), "wb") as f: 91 | f.write(rendered_post.encode("utf-8")) 92 | 93 | # Create the RSS feed 94 | with open(os.path.join(output_directory, "rss.xml"), "wb") as f: 95 | f.write(render_rss_page(posts).encode("utf-8")) 96 | 97 | 98 | if __name__ == "__main__": 99 | main(sys.argv[1]) 100 | -------------------------------------------------------------------------------- /src/posts/handling-2x-traffic-in-a-week.md: -------------------------------------------------------------------------------- 1 | title: How Khan Academy Successfully Handled 2.5x Traffic in a Week 2 | published_on: May 9, 2020 3 | author: Marta Kosarchyn 4 | team: Engineering 5 | ... 6 | Talk about rapid scaling... 7 | 8 | A few months ago I posted [some thoughts on scaling](https://engineering.khanacademy.org/posts/eng-principles-help-scale.htm) and promised to post more soon. Well, talk about rapid scaling — within just two weeks in March, Khan Academy site usage grew to 2.5x what it was at the same time last year and has sustained that level to date. As schools all over the world closed because of the coronavirus pandemic and students, parents, and teachers moved to distance learning, Khan Academy was able to respond, offering high-quality content and classroom experience — for free. In the month of April, we served 30 million learners on our platform. A recent national survey of parents found that [Khan Academy was the “most used online resource”](https://tytonpartners.com/library/2177-2/). 9 | 10 | I’m proud that we absorbed this rapid growth without disrupting our users. In addition to reacting quickly to alleviate pressure points within a few days, we had prepared in advance, and that preparation paid dividends. We scaled readily in large part because of our architecture and a rigorous practice of choosing external services carefully and using them properly. 11 | 12 | So in this post I’ll discuss architectural aspects that play a key role in the scalability of our site. 13 | 14 | Two fundamental components of our architecture serve us particularly well here. We use [Google Cloud](https://cloud.google.com/appengine), including AppEngine, Datastore, and Memcache, and [Fastly CDN](https://www.fastly.com/products/cdn), and they were the backbone of the **serverless and caching strategy that’s key to our scalability**. 15 | 16 | 17 | ![Architecture Diagram](../images/scaling-traffic-in-a-week.png High-level architecture) 18 | 19 | 20 | ## Serverless infrastructure 21 | 22 | Using GCP App Engine, a fully managed environment, means we can scale very easily with virtually no effort. Even with a substantial traffic increase, our site stayed up and performed well, with minimal intervention. We didn’t need to worry about load balancing ourselves because server instances were brought up as needed without any intervention. We similarly use Datastore which scales out storage and access capacity automatically in much the same way App Engine scales out web server instances. 23 | 24 | ## Caching 25 | 26 | Fastly CDN allows us to cache all static data and minimize server trips. Huge for scalability, it also helps us optimize hosting resources, for which costs grow linearly with usage in our App Engine serverless model. As shown in the architecture diagram, all client requests go through Fastly so we can prevent unnecessary server traffic, improving performance. We load videos primarily from YouTube and secondarily from Fastly. This also keeps costs down as well as ensures that the videos load quickly. 27 | 28 | In addition to caching static data in Fastly, we also extensively cache common queries, user preferences, and session data, and leverage this to speed up data fetching performance. We use Memcache liberally, in addition to exercising other key best practices around Datastore to ensure quick response times. 29 | 30 | Our site reliability (SRE) team of course needed to be prepared with ironclad monitoring - and we were. We noticed some slowdowns in the first few days and found that deploys were causing those hits. At our request, Google increased our Memcache capacity, and within a week we were comfortable returning to our normal continuous deployment pattern. This speed was critical, as our teams were quickly developing [resources](https://keeplearning.khanacademy.org/) to guide new site users in onboarding as easily as possible. 31 | 32 | Overall, we work hard to choose services carefully, follow best practices, and develop our own as needed. With the right technology, careful preparation, and adjustments on the spot by our amazing engineering team, we’ve been able to serve the students, parents, and teachers who rely on us now more than ever without interruption. 33 | 34 | ----- 35 | 36 | Khan Academy's increased usage has also increased our hosting costs, and we're a not-for-profit that relies on [philanthropic donations from folks like you](https://www.khanacademy.org/donate). 37 | -------------------------------------------------------------------------------- /src/post.py: -------------------------------------------------------------------------------- 1 | """Contains logic that deals with many of the details of rendering posts.""" 2 | 3 | import datetime 4 | import os 5 | 6 | import markdown 7 | from docutils.core import publish_parts 8 | import yaml 9 | 10 | import info 11 | 12 | 13 | def render_rst(text): 14 | """Renders some restructured text and returns generated HTML.""" 15 | 16 | docutils_settings = { 17 | # I don't want

tags in the post 18 | "initial_header_level": 2, 19 | 20 | # I don't want the docutils class added to every element 21 | "strip_classes": "docutils", 22 | 23 | "syntax_highlight": "short" 24 | } 25 | 26 | parts = publish_parts(text, writer_name="html", 27 | settings_overrides=docutils_settings) 28 | 29 | return parts["html_body"] 30 | 31 | 32 | def datetime_to_html_string(dt): 33 | # Use a shorter string when we're including the year. We could solve this 34 | # by wrapping the date in the side bar, but I think consistently using the 35 | # shorter form for dates that aren't from this year should be reasonable. 36 | if datetime.datetime.today().year != dt.year: 37 | return "{} {}, {}".format(dt.strftime("%b"), dt.day, dt.year) 38 | 39 | month = dt.strftime("%B") 40 | 41 | day_suffix = "th" 42 | if not (11 <= dt.day <= 13) and 1 <= dt.day % 10 <= 3: 43 | day_suffix = { 44 | 1: "st", 45 | 2: "nd", 46 | 3: "rd" 47 | }[dt.day % 10] 48 | day = "{}".format(str(dt.day), 49 | day_suffix) 50 | 51 | return "{} {}".format(month, day) 52 | 53 | 54 | def parse_frontmatter(post_contents): 55 | FRONT_MATTER_END = u"\n...\n" 56 | 57 | end = post_contents.find(FRONT_MATTER_END) 58 | if end == -1: 59 | return (None, post_contents) 60 | 61 | return (yaml.load(post_contents[:end]), 62 | post_contents[end + len(FRONT_MATTER_END):]) 63 | 64 | 65 | class Post(object): 66 | def __init__(self, path): 67 | with open(path, "rb") as f: 68 | frontmatter, content = parse_frontmatter(f.read().decode("utf-8")) 69 | 70 | # The path of the post file (ie: the RST file, not the result HTML 71 | # file). 72 | self.file_path = path 73 | 74 | self.title = frontmatter["title"] 75 | self.team = frontmatter["team"] 76 | self.published_on = ( 77 | datetime.datetime.strptime(frontmatter["published_on"], 78 | "%B %d, %Y")) 79 | self.author = frontmatter["author"] 80 | self.async_scripts = frontmatter.get("async_scripts", []) 81 | self.postcontent_scripts = frontmatter.get("postcontent_scripts", []) 82 | self.stylesheets = frontmatter.get("stylesheets", []) 83 | self.raw_content = content 84 | 85 | def get_html_content(self): 86 | """Processes the raw content and returns HTML.""" 87 | if self.file_path.endswith(".rst"): 88 | return render_rst(self.raw_content) 89 | elif self.file_path.endswith(".md"): 90 | return markdown.markdown( 91 | self.raw_content, 92 | extensions=["markdown.extensions.footnotes", 93 | "markdown.extensions.fenced_code", 94 | "markdown.extensions.codehilite"]) 95 | else: 96 | raise ValueError( 97 | "Unknown post type (file_path=%r)" % self.file_path) 98 | 99 | def get_output_name(self): 100 | name, ext = os.path.splitext(os.path.basename(self.file_path)) 101 | return name + ".htm" 102 | 103 | def to_dict(self): 104 | return { 105 | "title": self.title, 106 | "team_class": 107 | "team-" + self.team.lower().replace(" ", "-"), 108 | "published_on_html": 109 | datetime_to_html_string(self.published_on), 110 | "author": info.authors[self.author], 111 | "async_scripts": self.async_scripts, 112 | "postcontent_scripts": self.postcontent_scripts, 113 | "permalink": "/posts/" + self.get_output_name(), 114 | "stylesheets": self.stylesheets, 115 | } 116 | -------------------------------------------------------------------------------- /src/posts/no-cheating-allowed.rst: -------------------------------------------------------------------------------- 1 | title: No cheating allowed!! 2 | published_on: August 17, 2015 3 | author: Phillip Lemons 4 | team: Web Frontend 5 | ... 6 | 7 | The problem 8 | ============ 9 | Recently, a number of students on Khan Academy found a way to cheat by taking 10 | hints offline and not having them counted towards their online profile. When 11 | going through exercises on Khan Academy you answer the problems given to you 12 | and receive feedback on whether your answer was correct or incorrect. If you 13 | get stuck on a problem you are able to take hints and have that problem counted 14 | as incorrect. Check out `this exercise `_ 15 | if you want to try it yourself. The images below show the user getting a correct 16 | answer and taking a hint respectively. 17 | 18 | .. image:: /images/no-cheating-allowed/Correct_Screen.png 19 | :alt: Correct answer screenshot 20 | :width: 100% 21 | :align: center 22 | 23 | .. image:: /images/no-cheating-allowed/TakingHint_Screen.png 24 | :alt: Taking a hint screenshot 25 | :width: 100% 26 | :align: center 27 | 28 | The cheaters realized that if they disconnected from the internet, took the 29 | hints, and reconnected, they would still have a problem counted as correct. 30 | Taking offline hints worked this way because our servers expect a request from 31 | the client when users take a hint or answer a problem. If the users were 32 | disconnected from the internet the server would never see the request and the 33 | request was not stored anywhere on the client so it would be lost. 34 | 35 | How did we fix this? 36 | ==================== 37 | In order to address the offline cheating, we decided to change how the client 38 | sends requests to the server. By utilizing the client’s local storage, we could 39 | store failed requests to be retried once the user reconnected to the internet. 40 | This solution has the added benefit of removing the need for the client to be 41 | connected to the server all the time. Users with a spotty internet connection 42 | would have a better experience because everything would work even if the 43 | internet cut out for a short period of time. 44 | 45 | In our new architecture, anytime a user performs an action a string representing 46 | that action is stored in a queue that is saved to localstorage. When the queue 47 | is consumed, each action is mapped to a function that implements the action. 48 | This approach allows us to have more control over what happens when a request is 49 | not received by the server. The new queue retries any actions that fail and 50 | implements a linear backoff function so as not to be constantly sending requests 51 | when the user is not connected to the internet. 52 | 53 | Below is an image that shows the old architecture (left) and the new architecture 54 | (right). If the old client never received a response from the server the request 55 | would never be retried. In the new architecture the request is retried until it 56 | reaches the server and we get a response. 57 | 58 | .. image:: /images/no-cheating-allowed/HintClientArch.png 59 | :alt: Architecture screenshot 60 | :width: 100% 61 | :align: center 62 | 63 | A nice consequence of this architecture is that it can be generalized to work in 64 | other parts of our system. Code that deals with sending requests to the server 65 | can be updated to use this architecture and work more consistently even with a 66 | bad internet connection. Supporting an offline mode also becomes a possibility 67 | because you can just save all of the actions the user makes and send them to the 68 | server at a later time when the user has reconnected to the internet. 69 | 70 | The downside 71 | ============ 72 | One of the biggest downsides with this implementation is that with some editing 73 | of the user’s local storage, a hint request can be erased from the action queue. 74 | We decided this was acceptable for a couple of reasons. First, our typical 75 | classroom user is unlikely to know how to edit their local storage. Second, even 76 | if the user edits their local storage, it is visually obvious to those in the same 77 | room that they are up to something. A teacher can easily see students messing 78 | around with the chrome devtools and act accordingly. 79 | 80 | Conclusion 81 | ========== 82 | A client based architecture makes for a much better user experience because a 83 | spotty connection does not create a barrier to using our application. In our 84 | case, it also made it much harder to cheat on exercise problems and was a great 85 | way to make server requests more reliable. Additionally, this architecture 86 | makes having an offline mode more feasible. 87 | 88 | -------------------------------------------------------------------------------- /src/posts/evil-puzzle.rst: -------------------------------------------------------------------------------- 1 | title: How wooden puzzles can destroy dev teams 2 | published_on: July 6, 2015 3 | author: John Sullivan 4 | team: Web Frontend 5 | ... 6 | 7 | Last week a mysterious double-sided puzzle appeared at `Khan Academy `_. 8 | 9 | .. image:: /images/mysterious-puzzle.jpg 10 | :alt: A picture of the mysterious puzzle 11 | :width: 75% 12 | :align: center 13 | :target: /images/mysterious-puzzle.jpg 14 | 15 | To solve the puzzle you must fit all four pieces inside the recessed area (the pieces will not entirely fill the area). We found a solution to the easy side after only a few days [#easy_solution]_ but nobody could get close to solving the hard side. So **five** of us at Khan Academy began writing our own solvers. 16 | 17 | The first question we each faced was how to best represent the positions of the pieces. Laying a triangular grid over each side was intuitive enough, but it wasn't obvious how the cells should be addressed. 18 | 19 | Naturally we all came up with different systems [#such_coordinates]_. I went with a system that used two perpendicular axes, with the origin at the bottom left. 20 | 21 | .. image:: /images/triangular-grid.png 22 | :alt: An image of the triangular grid 23 | :width: 75% 24 | :align: center 25 | 26 | .. image:: /images/johns-coordinates.png 27 | :alt: An image illustrating my coordinate system 28 | :width: 75% 29 | :align: center 30 | :target: /images/johns-coordinates.png 31 | 32 | I had a problem though. Once I manually input a piece into this coordinate system, I needed to rotate and reflect that piece into 12 different alignments. Reflection was easy, but despite my best efforts, I couldn't figure out how to rotate the pieces programmatically once they were placed into my grid. 33 | 34 | After smashing my head against the problem for an hour and getting nowhere, I gave up [#emily_rotation]_ and manually inputted the three rotations necessary for each piece (all the other alignments could be expressed as reflections of those rotations). 35 | 36 | Now I just had to write the logic to try every possible placement of the pieces, but I was behind. 37 | 38 | Ben Eater had already finished `his solver `_ and it was churning away. His solver didn't do any pruning of the search space though (and took some time to check each placement), so he estimated that the solver would finish in around 2 years. I felt good about my chances of finding a solution before then. 39 | 40 | .. image:: /images/eaters-solver.gif 41 | :alt: Ben Eater's solver 42 | :align: center 43 | 44 | To try and be a little faster I added in some logic to skip large parts of the search space where possible. This worked by laying down a piece at a time, and only trying the other ones if there were no collisions. 45 | 46 | For example, first my program would lay down Piece A somewhere. If Piece A collided with a wall, my program would not try laying down Piece B yet, but would instead move Piece A somewhere else. 47 | 48 | This ended up working well and soon I had `a solver `_ that could brute force the puzzle in less than a minute. 49 | 50 | .. image:: /images/solver.gif 51 | :alt: My solver 52 | :width: 50% 53 | :align: center 54 | 55 | `Emily Eisenberg `_ finished `her solver `_ around the same time and we were able to confirm our results. **The hard side of the puzzle was unsolvable**. 56 | 57 | Clearly there was a very evil puzzle master in our ranks. 58 | 59 | .. image:: /images/evil-kitty.gif 60 | :alt: An evil kitten 61 | :width: 50% 62 | :align: center 63 | 64 | `Jamie Wong `_ readily admitted to bringing in the puzzle, but despite the staggering proof to the contrary, he was adamant that a solution existed. He said our solvers all shared a fatal flaw. 65 | 66 | After a few hints, Emily and I did find the answer [#hard_solution]_. Which was good, because none of us had gotten any work done for awhile. 67 | 68 | .. [#easy_solution] If you want to spoil it for yourself, here is `a picture of the solved easy side `_. 69 | .. [#such_coordinates] Ben Eater decided to side-step the issue by drawing the shapes directly onto the screen. Cam Christensen came up with a coordinate system with two axes that formed a 60° angle and he convinced Emily Eisenberg to use the same system. `Justin Helps `_ used a vertex-based coordinate system (rather than piece-based) that made rotation and reflection easy, but collision detection super hard. 70 | .. [#emily_rotation] `Emily, however, was able to easily figure out rotation `_ 71 | .. [#hard_solution] You don't really want me to give you the answer do you? That would be boring. 72 | -------------------------------------------------------------------------------- /src/posts/new-oss-activity.md: -------------------------------------------------------------------------------- 1 | title: "What's New in OSS at Khan Academy" 2 | published_on: April 3, 2017 3 | author: Brian Genisio 4 | team: Web Frontend 5 | ... 6 | 7 | At Khan Academy, we rely heavily on Open Source Software (OSS). The majority of our software is built upon OSS and we love giving back to the community when we find nuggets of code we think other will find useful. 8 | 9 | If you're ever curious to see what we're up to with OSS, take a look at [https://khan.github.io](https://khan.github.io). Some of the most recent additions to that page are highlighted below. 10 | 11 | ## Mu Lambda 12 | [Mu Lambda](https://github.com/khan/mu-lambda) is a small library of functional programming utilities for JavaScript. Our front-end codebase is using more and more functional concepts lately and we need some help composing our functions via utilities. Unfortunately, most of the great OSS options out there today are rather heavy-weight from a payload perspective. Mu Lambda is 1/10th the size of the popular options out there! One caveat: it doesn't have monad support (which is partially how we reduce the payload size.) If you want to see it in action, check out the [tests](https://github.com/Khan/mu-lambda/blob/master/test/test.js). 13 | 14 | ## React Balance text 15 | [React Balance Text](https://github.com/khan/react-balance-text) is a React wrapper for the Adobe Web Platform's [Balance-Text](https://github.com/adobe-webplatform/balance-text) project which aims to help eliminate [text rags and widows](https://www.fonts.com/content/learning/fontology/level-2/text-typography/rags-widows-orphans) in page copy by making sure the text is as "balanced" as possible across all lines. 16 | 17 | Before we built our wrapper, we contributed to their project to help remove the jQuery dependency. It didn't seem prudent to have a React component which relied on jQuery. In today's browser ecosystem, a component like Balance-Text no longer needs anything that jQuery can offer. We collaborated with Adobe to create version 3.0 of Balance-Text and then wrote a simple wrapper around it for React. 18 | 19 | The usage is simple and clean: 20 | 21 | ``` 22 | 23 | Dispassionate extraterrestrial observer another world, as a patch of light! Extraplanetary! Colonies great turbulent clouds Orion's sword. Venture, circumnavigated vastness is bearable only through love? Tendrils of gossamer clouds quasar cosmos a still more glorious dawn awaits quasar decipherment Drake Equation citizens of distant epochs cosmic ocean consciousness, are creatures of the cosmos not a sunrise but a galaxyrise galaxies, colonies vastness is bearable only through love as a patch of light shores of the cosmic ocean and billions upon billions upon billions upon billions upon billions upon billions upon billions. 24 | 25 | ``` 26 | 27 | See the [project's storybook](https://khan.github.io/react-balance-text/) for more details and use cases. 28 | 29 | ## Fuzzy Match Utils 30 | [Fuzzy Match Utils](https://github.com/Khan/fuzzy-match-utils) is a collection of string matching algorithms designed with [React Select](https://github.com/JedWatson/react-select) in mind. Internally, it uses the [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) and [longest common subsequence](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem) algorithms which employs [dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming) to measure the difference between two string sequences. Instead of matching strings literally, it uses a "fuzzy" approach. For example, if you searched for "Bay High", it will do well to find "Bayside High School" in a list of schools. We use these utilities in some of our React Select implementations to help the user filter long lists of options more effectively. 31 | 32 | ## React Multi-Select 33 | Speaking of React Select, [React Multi Select](https://github.com/Khan/react-multi-select) is a multiple select component for React (which also uses Fuzzy Match Utils), modeled after the React Select look and feel. React Select is awesome, and we use it a lot, but the way it handles multiple options didn't jive with some new user experiences we are working on. React Select uses a tagging approach where we preferred a dropdown of checkboxes. 34 | 35 | We wrote React Multi Select to have the same look and feel as React Select, but with a different UX when the user selects the dropdown, specifically designed for the multiple selection use case. We used the [React Component Development Kit](https://github.com/storybooks/react-cdk) which uses [React Storybook](https://github.com/storybooks/react-storybook) to help us show use cases. Because of this, you can demo the component in the [project's storybook](https://khan.github.io/react-multi-select/) 36 | 37 | ![animation of react-multi-select](/images/new-oss-activity/react-multi-select.gif) 38 | 39 | ## Keeping everything else alive 40 | In addition to these new projects, we've been keeping our other OSS projects updated as well. For example, we've landed several performance-related updates to [Aphrodite](https://github.com/Khan/aphrodite) thanks to public contributor [Joe Lencioni](https://github.com/lencioni). In [KaTeX](https://github.com/Khan/KaTeX), we've also incorporated several pull requests from the community. Check out our [OSS page](http://khan.github.io) for more projects. 41 | -------------------------------------------------------------------------------- /src/posts/eng-principles-help-scale.md: -------------------------------------------------------------------------------- 1 | title: How Engineering Principles Can Help You Scale 2 | published_on: August 21, 2019 3 | author: Marta Kosarchyn 4 | team: Engineering 5 | ... 6 | 7 | Our engineering team has grown a lot over the past couple of years, and we’re 8 | delivering more product than ever before. It’s exhilarating. It’s also tricky. 9 | Because scaling is hard. This is the first of a set of posts in which we’ll 10 | talk about how we’re managing it at Khan Academy. 11 | 12 | Like many engineering organizations experiencing rapid growth, we’ve used some 13 | standard strategies for scaling over the past two years: 14 | 15 | - Technology and architecture—leaner and cleaner 16 | - Decision-making and communication—be open, keep it simple, repeat often 17 | - People and organization—make sure the right person is in the right seat 18 | - Process and tools—standard and common across the team 19 | 20 | These are effective levers, and they’ve been helpful as we’ve grown from a small 21 | engineering team to a not-so-small engineering team, moving ever faster and 22 | delivering more and more. Stay tuned for more on how we’ve used them. 23 | 24 |
25 | 26 |
27 | Making the right shifts keeps things running smoothly
28 | (Photo by Alok Sharma on Unsplash) 29 |
30 |
31 | 32 | But today’s post is about what I believe may be the most critical aspect of 33 | scaling, the one that really determines how fast and smooth a growth journey 34 | will be—evolving your engineering principles so they support hyperscaling while 35 | keeping your culture intact and your team vitally engaged. 36 | 37 | Getting principles right has an accelerating effect on all other levers of 38 | change as well as positive impact on growth and professional development 39 | opportunities for engineers. It also results in a sustainable codebase, faster 40 | velocity, and—most importantly—happy and productive engineers. 41 | 42 | Khan Academy’s initial engineering principles were key to the success of our 43 | early years as an organization. They reflected the nimbleness necessary in a 44 | team bootstrapping a foundation for reaching the vast global community of 45 | learners whose lives could be changed with free access to a world-class 46 | education. Those initial principles were aimed at removing any barriers to 47 | speed. They encouraged, for example, anyone to work on any of the code, anytime. 48 | They were effective, and we grew in both impact and team size. We grew a lot. We 49 | grew to a size that meant alignment with our original principles was slowing us 50 | down. We became more likely to break things and to leave behind clutter in the 51 | code. Architectural decisions were made implicitly and on the fly and were not 52 | logged. Ultimately our pace of delivery slowed. That’s when we identified the 53 | need to correct course and embarked on a journey of turning things around: it 54 | was time to reexamine our engineering principles. 55 | 56 |

Getting principles right has an accelerating effect on all 57 | other levers of change

58 | 59 | We had to refocus on what it takes to build high quality and sustainable code 60 | when there are 10 times as many engineers working on the product, when the 61 | product has expanded to include test prep (we have over 50% of the market for 62 | [SAT prep](https://blogs.edweek.org/edweek/high_school_and_beyond/2017/05/college_board_reports_score_gains_from_free_sat_practice.html)) 63 | and enabling [teachers and students in the classroom](https://www.latimes.com/socal/glendale-news-press/news/tn-gnp-me-gusd-khan-academy-learning-20190712-story.html). 64 | And continue to publish more and more world-class content - now in more than 40 65 | languages and across more than 370 courses. 66 | 67 | 68 | 69 | Here are our [updated Khan Academy Engineering Principles.](https://docs.google.com/presentation/d/1ZQ-HTuH38L8sf4ZObfJNlESpzYIPAP6QFe30qOqE8DA/edit?usp=sharing) 70 | A very thoughtful working group of Khan engineers, architects, and managers 71 | focused on what would guide us best in this next phase of our journey. The 72 | principles represent how we work together as a community focused on growing a 73 | world-class engineering team that delivers on a mission to provide a free 74 | world-class education to anyone, anywhere. We champion quality (sometimes we go 75 | slow to go fast), we nurture every engineer (everyone learns and everyone 76 | grows), and we collaborate compassionately (we work as an efficient team and we 77 | treat each other with utmost respect). The principles remind us that the impact 78 | we make on our community and on the world should be no less than spectacular. 79 | 80 | I’m excited to share [our new engineering principles](https://docs.google.com/presentation/d/1ZQ-HTuH38L8sf4ZObfJNlESpzYIPAP6QFe30qOqE8DA/edit?usp=sharing) 81 | with you. Please read. We’ll continue to unpack them in coming posts. And, we’ll 82 | talk more about how the new principles are guiding us through a re-architecture 83 | journey and other key foundation strengthening initiatives. 84 | 85 | If you find our engineering principles are the kind that you want to hold 86 | yourself to, come join us—[we’re hiring](https://www.khanacademy.org/careers)! 87 | -------------------------------------------------------------------------------- /src/gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ // TODO(csilvers): does gulp support const? 2 | 3 | var fs = require("fs"); 4 | 5 | var concat = require("gulp-concat"); 6 | var connect = require("gulp-connect"); 7 | var foreach = require("gulp-foreach"); 8 | var gulp = require("gulp"); 9 | var imagemin = require("gulp-imagemin"); 10 | var inject = require("gulp-inject"); 11 | var less = require("gulp-less"); 12 | var minifyCss = require("gulp-minify-css"); 13 | var minifyHTML = require("gulp-minify-html"); 14 | var minifyInline = require("gulp-minify-inline"); 15 | var path = require("path"); 16 | var shell = require("gulp-shell"); 17 | var argv = require("yargs").argv; 18 | 19 | // Prefer PYTHON from the virtualenv, but if it doesn't exist, just 20 | // use the system python. 21 | var PYTHON = "../env/bin/python"; 22 | try { 23 | fs.statSync(PYTHON); 24 | } catch (e) { 25 | PYTHON = "python"; 26 | } 27 | 28 | /** 29 | * Runs the Phial app to generate the site. 30 | * 31 | * TODO(johnsullivan): Don't use a hardcoded directory. 32 | */ 33 | gulp.task("phial", shell.task([ 34 | "rm -rf /tmp/engblog-phial", 35 | "mkdir /tmp/engblog-phial", 36 | PYTHON + " ./app.py /tmp/engblog-phial", 37 | ])); 38 | 39 | function inlinePostCss(inputGlob, outputDir) { 40 | return gulp.src(inputGlob) 41 | .pipe(foreach(function(stream, file) { 42 | // Gather all of the CSS we want to inline in this post 43 | var css = (gulp 44 | .src(["styles/post-template.less", 45 | "../node_modules/normalize.css/normalize.css", 46 | "styles/pygments.css"]) 47 | .pipe(less()) 48 | .pipe(concat("all.css"))) 49 | .pipe(minifyCss()); 50 | 51 | return stream 52 | .pipe(inject(css, { 53 | starttag: "", 54 | transform: function(filePath, file) { 55 | // return file contents as string 56 | return ( 57 | ""); 59 | }, 60 | })); 61 | })) 62 | .pipe(minifyHTML({loose: true})) 63 | .pipe(minifyInline({css: false})) 64 | .pipe(gulp.dest(outputDir)); 65 | } 66 | 67 | /** 68 | * Embeds each post page's CSS. 69 | */ 70 | gulp.task("inline-css", gulp.series(["phial"], function() { 71 | return inlinePostCss("/tmp/engblog-phial/posts/*", "../output/posts/"); 72 | })); 73 | 74 | /** 75 | * The index page (which is just one of the posts) needs the same treatment 76 | */ 77 | gulp.task("inline-index-css", gulp.series(["phial"], function() { 78 | return inlinePostCss("/tmp/engblog-phial/index.htm", "../output/"); 79 | })); 80 | 81 | /** 82 | * Move the RSS feed into the output directory. 83 | */ 84 | gulp.task("rss-feed", gulp.series(["phial"], function() { 85 | // TODO(johnsullivan): Minify this. Stripping whitespace is probably the 86 | // only safe thing we can do. 87 | return gulp.src("/tmp/engblog-phial/rss.xml").pipe(gulp.dest("../output")); 88 | })); 89 | 90 | /** 91 | * Shortcut task to create the site's content. 92 | */ 93 | gulp.task("content", gulp.series(["inline-css", "inline-index-css", "rss-feed"], 94 | function(done) { done(); })); 95 | 96 | /** 97 | * Moves all of the images into the output directory (and optimizes them). 98 | */ 99 | gulp.task("images", function() { 100 | var source = gulp.src("images/**"); 101 | 102 | if (argv.production) { 103 | source = source.pipe( 104 | imagemin({optimizationLevel: 5, progressive: true})); 105 | } 106 | 107 | return source.pipe(gulp.dest("../output/images")); 108 | }); 109 | 110 | /** 111 | * Moves all of the videos into the output directory. 112 | */ 113 | gulp.task("videos", function() { 114 | return gulp.src("videos/**") 115 | .pipe(gulp.dest("../output/videos")); 116 | }); 117 | 118 | gulp.task("supporting-files", function() { 119 | return gulp.src("supporting-files/**") 120 | .pipe(gulp.dest("../output/supporting-files")); 121 | }); 122 | 123 | gulp.task("javascript", function() { 124 | return gulp.src("javascript/**") 125 | .pipe(gulp.dest("../output/javascript")); 126 | }); 127 | 128 | gulp.task("default", 129 | gulp.parallel(["content", "images", "videos", "supporting-files", "javascript"], 130 | function(done) { done(); } 131 | )); 132 | 133 | gulp.task("watch", function(done) { 134 | gulp.watch(["**"], gulp.parallel( 135 | ["content", "images", "videos", "supporting-files"])); 136 | done(); 137 | }); 138 | 139 | gulp.task("connect", gulp.series(["default"], function(done) { 140 | connect.server({ 141 | // Uncomment this line to expose the webserver to your private 142 | // network (you would never do this on an unsafe public network 143 | // would you?). 144 | // host: "0.0.0.0", 145 | livereload: true, 146 | port: 9103, 147 | root: path.resolve(path.join(__dirname, '..', 'output')), 148 | fallback: path.resolve(path.join(__dirname, '..', 'output', 'index.htm')), 149 | open: "", 150 | }); 151 | done(); 152 | })); 153 | 154 | /** 155 | * Build and serve the site for testing. 156 | */ 157 | gulp.task("serve", gulp.parallel(["connect", "watch"])); 158 | -------------------------------------------------------------------------------- /src/posts/its-okay-to-break-things.md: -------------------------------------------------------------------------------- 1 | title: "It's Okay to Break Things: Reflections on Khan Academy's Healthy Hackathon" 2 | published_on: March 6, 2017 3 | author: Kimerie Green 4 | team: Web Frontend 5 | ... 6 | For the past few months, I have been working as a Software Engineering Fellow at Khan Academy. This program gives engineers from non-traditional backgrounds the opportunity to build their experience by working on real products alongside full-time engineers. During my time as a fellow, I’ve had the opportunity to work with amazing engineers and work on projects that have had immediate impact on Khan Academy’s users. I was attracted to this role because of my own background working for education nonprofits. The fellowship has given me tangible engineering experience while allowing me to pursue my passion for increasing educational equity for all learners. 7 | 8 | A few weeks ago, we had our internal [Healthy Hackathon](http://healthyhackathon.khanacademy.org/) where everyone across the company worked on projects related to anything from improving KA products, creating internal tools to make everyone’s lives easier, creating applications to improve greater society (beyond education), or anything that fosters curiosity, collaboration, and fun. 9 | 10 | I worked on a project called “Read-to-Me” that used Mozilla’s [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API) to read widget content created in [Perseus](https://github.com/Khan/perseus) aloud in our [Early Learner](https://www.khanacademy.org/math/early-math) products. This minor improvement allows young learners who are still building their reading skills to have additional support when completing exercises on our platform. I was excited to work on a project that would better support all early learners regardless of their backgrounds. Many of the gaps in achievement that form between low-income and students of color and their more affluent counterparts begin in early childhood. This project aligned perfectly with my desire to increase equity in education. Additionally, I was able to work with/learn from two experienced engineers and play around with a really cool experimental web technology! 11 | 12 | I had so much fun working on this project, but it wasn’t without challenges. Perseus is one of the most complicated parts of our codebase, and it was hard to decide where and how the API should be used in existing code that had a lot of complexity. We didn’t end up shipping Read-to-Me (it definitely needs a few rounds of code review and some design help), but I am really proud of what we were able to achieve. Through this project, I learned a few lessons that I hope will continue to support me as I continue to grow and reach my fullest potential as a software engineer (and hopefully this will be helpful others too): 13 | 14 | **Endless Curiosity Beats Getting the Answer Right Away:** One way that I have learned to persevere through tough spots is by channeling my curiosity about the problem I am trying to tackle. I spent a lot of time looking at Perseus code (probably too much time) figuring out how it works under-the-hood and mapping it back to behaviors I saw in the exercise editor . A lot of it didn’t make sense, but eventually, I was able to figure out a suitable place in the code to implement the API. It wasn’t perfect, but it ended up being enough to help my hackathon team move forward with our project. There’s a world of technical challenges out there just waiting to be solved. Be open and willing to dive in even if it means going through multiple iterations before reaching a solution. 15 | 16 | **Try it. You’ll Like it:** I’ve been very hesitant to try experimental technologies. Because I am early in my career as an engineer, I fall into a trap of wanting to become really good at React or some other established technology before diving into something new. The reality is one could spend a lifetime learning to be really good at something, and engineering is one of those professions where one must constantly learn new things (or even refresh on old concepts). Playing around with the Web Speech API made me more excited to explore other experimental technologies in web development. I learned that as I continue to build upon fundamentals, I should make space for joy, fun and exploration in coding. 17 | 18 | **It’s Okay to Break Things:** A part of learning, growing, and understanding tools/technologies is being in the muck before we have clarity around how something works, where something should go, and how something should be built. Sometimes we don’t know the path forward, and we still have to be okay with saying, “onward” until a solution crystallizes. When learning, we must break things and experience confusion. I’m pretty sure I spent most of the hackathon debugging weird error messages as a part of figuring out how Perseus works than I did creating an elegant solution. It was only by breaking something that I was able to figure out how to move forward. This lesson will probably be the most difficult to live out everyday in my work because failure is hard, even for individuals who have a cultivated a growth mindset for a long time. Although, I don’t intend to take down an entire website for the sake of learning, there’s something to be said for having the courage to try things even if it means they won’t necessarily work out. There’s so much learning in our shortcomings. I hope I have more of these moments because it means I’m learning and growing not only as an engineer, but also as a human. 19 | 20 | Who knew I would gain so much from a hackathon! I’ve had an amazing experience so far as a Fellow at Khan Academy, and I hope to carry these lessons and so much more throughout my career. Onward! 21 | -------------------------------------------------------------------------------- /src/posts/introducing-swifttweaks.md: -------------------------------------------------------------------------------- 1 | title: "Introducing SwiftTweaks" 2 | published_on: May 9, 2016 3 | author: Bryan Clark 4 | team: Mobile 5 | ... 6 | 7 | Today, we’re releasing [SwiftTweaks](https://github.com/khan/SwiftTweaks), a way to adjust your Swift-based iOS app without needing to recompile. 8 | 9 | ![Overview of SwiftTweaks](/images/introducing-swifttweaks/overview.png) 10 | Your users won’t see your animation study, Sketch comps, or prototypes. What they *will* see is the finished product - so it’s really important to make sure that your app feels right on a real device! 11 | 12 | Animations that look great on your laptop often feel too slow when in-hand. Layouts that looks perfect on a 27-inch display might be too cramped on a 4-inch device. Light gray text may look subtle in Sketch, but it’s downright illegible when you’re outside on a sunny day. 13 | 14 | For these reasons, it’s helpful to fine-tune your designs on-device - but that’s a lot of work: open Xcode, tweak your code, and wait for the app to build to device before seeing the results. 15 | 16 | ### What about Facebook Tweaks? 17 | In Objective-C projects, I’ve cherished [Facebook’s Tweaks](https://github.com/facebook/tweaks), a tool that makes this process easy. However, while it's possible to use FBTweaks in Swift, it's far less convenient than in Objective-C. 18 | 19 | Since [Khan Academy](https://khanacademy.org)’s iOS code is almost entirely Swift, we wanted something that would make it easy to use tweaks. (Plus: with Swift’s generic types, protocols, and all-around awesomeness, we figured we could make some improvements.) 20 | 21 | We’ve been using SwiftTweaks for a few months now in our iOS app, and it’s been wonderful for fine-tuning gestures, adjusting animations, and toggling feature flags. 22 | 23 | ## Using SwiftTweaks 24 | ### Create a TweakLibrary 25 | First, you create a `TweakLibrary`, which contains `Tweaks` and a `TweakStore`. (If `TweakStore.enabled` is false, then the Tweaks UI will be inaccessible and all tweaks return their default value - which means you can leave this code in-place when you ship your production app.) 26 | 27 | ``` 28 | public struct ExampleTweaks: TweakLibraryType { 29 | public static let colorTint = Tweak("General", "Colors", "Tint", UIColor.blueColor()) 30 | public static let marginHorizontal = Tweak("General", "Layout", "H. Margins", defaultValue: 15, min: 0) 31 | public static let marginVertical = Tweak("General", "Layout", "V. Margins", defaultValue: 10, min: 0) 32 | public static let featureFlag = Tweak("Feature Flags", "Main Screen", "Show Body Text", true) 33 | 34 | public static let buttonAnimation = SpringAnimationTweakTemplate("Animation", "Button Animation") 35 | 36 | public static let defaultStore: TweakStore = { 37 | let allTweaks: [TweakType] = [colorTint, marginHorizontal, marginVertical, featureFlag] 38 | 39 | #if DEBUG 40 | let tweaksEnabled: Bool = true 41 | #else 42 | let tweaksEnabled: Bool = false 43 | #endif 44 | 45 | return TweakStore( 46 | tweaks: allTweaks.map(AnyTweak.init), 47 | enabled: tweaksEnabled 48 | ) 49 | }() 50 | } 51 | ``` 52 | 53 | 54 | 55 | ### Calling Tweaks in your code 56 | When you want to use a tweak in your code, use the `assign`, `bind,` and `bindMultiple` functions. 57 | 58 | **assign** returns the current value of the tweak: 59 | 60 | ``` 61 | button.tintColor = ExampleTweaks.assign(ExampleTweaks.colorTint) 62 | ``` 63 | 64 | **bind** calls its closure immediately, and again each time the tweak changes: 65 | 66 | ``` 67 | ExampleTweaks.bind(ExampleTweaks.colorTint) { button.tintColor = $0 } 68 | ``` 69 | 70 | **bindMultiple** calls its closure immediately, and again each time any of its tweaks change: 71 | 72 | ``` 73 | // A "multipleBind" is called initially, and each time _any_ of the included tweaks change: 74 | let tweaksToWatch: [TweakType] = [ExampleTweaks.marginHorizontal, ExampleTweaks.marginVertical] 75 | ExampleTweaks.bindMultiple(tweaksToWatch) { 76 | let horizontal = ExampleTweaks.assign(ExampleTweaks.marginHorizontal) 77 | let vertical = ExampleTweaks.assign(ExampleTweaks.marginVertical) 78 | scrollView.contentInset = UIEdgeInsets(top: vertical, right: horizontal, bottom: vertical, left: horizontal) 79 | } 80 | ``` 81 | 82 | There are also several handy `TweakGroupTemplate` types, to help you with commonly-tweaked things. Our above `ExampleTweaks` library included one for a `UIView` spring animation: 83 | 84 | ``` 85 | public static let buttonAnimation = SpringAnimationTweakTemplate("Animation", "Button Animation") 86 | ``` 87 | 88 | This single line of code creates four tweaks - for duration, delay, damping, and initial spring velocity. Each has sensible defaults (e.g. “delay can’t be negative”) - and there’s a `UIView` extension to easily use the `TweakGroup`: 89 | `UIView.animateWithSpringAnimationTweakTemplate` 90 | 91 | For more on using Tweaks and TweakGroupTemplates, [check out the example project](https://github.com/Khan/SwiftTweaks/blob/master/iOS%20Example/iOS%20Example/ViewController.swift). 92 | 93 | ### Accessing the interface 94 | Lastly, we need a way to adjust our Tweaks while the app is running. The simplest way is to set your app’s `UIWindow` to be a `TweakWindow`. By default, the `TweakWindow` presents a `TweaksViewController` when you shake the device in a debug build, but you can provide a different gesture recognizer, too. 95 | 96 | You can also handle the presentation of a `TweaksViewController` if you prefer to not use a `TweakWindow`. 97 | 98 | ### Tweaking values 99 | Now for the fun part - shake your phone, and your tweaks appear! Adjust booleans with a switch, numbers with a stepper or keyboard, and there’s a great color-editing interface in there, too! There’s also a “floating UI” so you can edit tweaks without leaving a screen. 100 | 101 | Here's a preview of the SwiftTweaks example app (included in the repository): 102 | ![animated demo](/images/introducing-swifttweaks/demo.gif) 103 | 104 | [Check it out on GitHub](https://github.com/khan/swifttweaks) and let us know what you think! 105 | -------------------------------------------------------------------------------- /src/posts/translation-server.md: -------------------------------------------------------------------------------- 1 | title: "Schr\u00F6dinger's deploys no more: how we update translations" 2 | published_on: October 12, 2015 3 | author: Chelsea Voss 4 | team: Infrastructure 5 | ... 6 | 7 | 8 | If you’re trying to bring the best learning experience to people around the world, it’s important to, well, think about the world. 9 | 10 | Khan Academy is translated into [Spanish](http://es.khanacademy.org/) and [Turkish](https://tr.khanacademy.org/) and [Polish](https://pl.khanacademy.org/) and more – and this includes not only text, but also the articles, exercises, and videos. Thanks to the efforts of translators, learners [around the world](http://international.khanacademy.org/) can use Khan Academy to learn in their language. 11 | 12 | !["You can learn anything" in several languages](/images/translation-server/language_collage.png) 13 | 14 | Internationalization is important. Internationalization is also an engineering challenge: it requires infrastructure to mark which strings in a codebase need to exist in multiple different languages, to store and look up the translated versions of those strings, and to show the user a different website accordingly. Additionally, since our translations are crowdsourced, we need infrastructure to allow translators to translate strings, to show translators where their effort is most needed, and to show these translations once they’re ready. There are many moving parts. 15 | 16 | When I arrived at Khan Academy at the beginning of this summer, some of these moving parts in our internationalization infrastructure were responsible for most of the time our deploys took to finish. One of the things I accomplished this summer during my internship here was to banish this slowness from our deploy times. 17 | 18 | The problem 19 | --- 20 | 21 | Whenever we download the latest translation data from [Crowdin](http://crowdin.com/), which hosts our crowdsourced translations, we rebuild the *translation files* – files which the Khan Academy webapp can read in order to show translated webpages. The next time an engineer deploys a new version of the webapp, these new translation files are then deployed as well. 22 | 23 | Uploading files to Google App Engine, which hosts the Khan Academy website, is usually the slowest part of our deploys; the translation files are big, so translation files in particular are a major contributor to this. So, whenever the latest translations are downloaded and rebuilt, the next deploy would be quite a bit slower while it uploaded the changed files. 24 | 25 | Furthermore, since it’s not always the case that translation files have been rebuilt recently, as an engineer it’s hard to tell whether the deploy you’re about to make will be hit with Translations Upload Duty or not. Sometimes deploys would take around 30 minutes, sometimes they would take around 75 minutes or more: 26 | 27 | ![Graph of deploy times, before translation server](/images/translation-server/graph_before.png) 28 | 29 | *The previous state of affairs – 30-minute deploys punctuated by 75-minute deploys. There are a couple of lulls in this graph where deploys are consistently near 30 minutes: these reflect times when our download from Crowdin was not working.* 30 | 31 | The fix 32 | --- 33 | 34 | We decided to rearrange the infrastructure around this so that instead of uploading translation files to Google App Engine (GAE) along with the rest of the webapp, we would upload the translation files to Google Cloud Storage (GCS) in a separate process and then modify the webapp to read the files from there. 35 | 36 | Implementing this required making a few different changes, and the changes had to be coordinated in such a way as to keep internationalized sites up and running throughout the entire process: 37 | 38 | 1. Upload the translation files to GCS whenever they're updated. 39 | 2. Change the webapp to read translations from GCS instead of from GAE. 40 | 3. Stop uploading the now-unnecessary translation files to GAE. 41 | 42 | These steps by themselves are enough to implement the change to make deploys faster, but we also want to make sure that this project won't break anything – so instead, the steps look something like: 43 | 44 | 1. Upload the translation files to GCS whenever they're updated. *Measure everything. (How much will this cost? How fast will the upload be?)* 45 | 2. Change the webapp to read translations from GCS instead of from GAE. *Measure everything. (How much slower is this than reading from disk? Will requests be slower?)* 46 | 3. Stop uploading the now-unnecessary translation files to GAE. *Measure everything. (Do translated sites still work?)* 47 | 48 | One thing I experimented with while working on this project was keeping a lab notebook of sorts in a Google Doc; this was where I went to record everything I learned, from little commands that might be useful later, to dependencies I had to install, to all of the measurements I ended up making. This was a good decision. This habit and these notes did in fact turn out to be useful, frequently. 49 | 50 | ![Mythbusters' Adam Savage: "Remember kids, the only difference between screwing around and science is writing it down."](/images/translation-server/science.jpg) 51 | 52 | 53 | The consequences 54 | --- 55 | 56 | ### Deploys are faster! 57 | 58 | I deployed the last piece of this project on August 20, 2015. Deploy times have been more consistent since: the graph of deploy times is free of the spikes that previously indicated translation uploads. 59 | 60 | ![Graph of deploy times, after translation server](/images/translation-server/graph_after.png) 61 | 62 | *Before and after; the change happened on 8/20.* 63 | 64 | ### Translations can be updated independently! 65 | 66 | Now we don't require an engineer to deploy new code in order to change the translations that appear on Khan Academy – translations are updated by a separate job. Also, this opens up new possibilities – we can now do exciting things like updating our languages independently of each other, and make it so that the time between when a translator makes a translation and when that translation shows up on the main site becomes even shorter. Our internationalization efforts will be able to push forward even faster! 67 | -------------------------------------------------------------------------------- /src/supporting-files/generate_pickle_guards.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | """A script to make files that will log when refactors cause pickling errors. 4 | 5 | This is intended for use when moving files around where we aren't sure 6 | what symbols might be pickled and what symbols might not be. 7 | 8 | We store pickled data in databases and in other caches, and when 9 | you pickle a class or function it stores the absolute path to the 10 | class/function: foo.bar.myfunc. If you move that class/function to 11 | another file, or otherwise rename it, then you can't unpickle the data 12 | anymore, because the unpickler tries to load foo.bar.myfunc and can't 13 | find it! 14 | 15 | We have code to handle this in pickle_util.py, but that code needs to 16 | know the old and new locations for each problematic symbol. When 17 | moving a bunch of files around there are so many symbols we can't be 18 | sure we've got them all. So we use this script as a safeguard. 19 | 20 | What it does is create a file under the old name with a big `import *` 21 | to import all the moved symbols under the old name. Then they can be 22 | found by the unpickler. But we log if we ever need to use this file, 23 | so we can find the problematic unpickles in the log and add them to 24 | pickle_util to be handled properly. 25 | 26 | Eventually, once we've cleared up all issues that the logs show up, 27 | we'll be able to delete all the files that this script creates. 28 | 29 | This uses `git` to determine what files have moved, that need 30 | pickle guards. If you do not use git, you will need to specify the 31 | files in some other way. 32 | """ 33 | 34 | import os 35 | import re 36 | import subprocess 37 | import sys 38 | 39 | 40 | _TOP_LEVEL_SYMBOL_RE = re.compile(r'^(?:def|class)\s+(\w+)\s*[(:]', 41 | re.MULTILINE) 42 | 43 | _PICKLE_LOGGER_CONTENTS = """\ 44 | import logging 45 | 46 | # __file__ probably ends with '.pyc', convert that to .py 47 | logging.error("Should not be importing %%s, update pickle_util.py" 48 | %% (__file__[:-1] if __file__.endswith('.pyc') else __file__)) 49 | 50 | %s 51 | """ 52 | 53 | 54 | def _module_for_file(filename): 55 | """Return the module-name corresponding to the given filename.""" 56 | base, _ = os.path.splitext(filename) 57 | if os.path.basename(base) == '__init__.py': 58 | base = os.path.dirname(base) 59 | else: 60 | base, _ = os.path.splitext(filename) 61 | module = base.replace(os.path.sep, '.') 62 | return module 63 | 64 | 65 | def _top_level_symbols(contents): 66 | """Return a list of all top-level classes/functions defined in fname.""" 67 | return sorted(_TOP_LEVEL_SYMBOL_RE.findall(contents)) 68 | 69 | 70 | def _should_write_pickle_logger(outfile, new_file): 71 | # If it is not a python file then just ignore it, without even warning 72 | if not outfile.endswith('.py'): 73 | return False 74 | 75 | if os.path.exists(outfile): 76 | print ('WARNING: ignoring %s (importing %s): %s already exists' 77 | % (outfile, new_file, outfile)) 78 | return False 79 | 80 | with open(new_file) as f: 81 | new_contents = f.read() 82 | 83 | if not _top_level_symbols(new_contents): 84 | print ('INFO: ignoring %s, it has no top-level symbols to forward' 85 | % new_file) 86 | return False 87 | 88 | print "Creating pickle logger for %s in %s" % (new_file, outfile) 89 | return True 90 | 91 | 92 | def _write_pickle_logger(outfile, new_file): 93 | """Write a file to outfile that imports everything from new_file.""" 94 | with open(new_file) as f: 95 | new_contents = f.read() 96 | 97 | what_to_import = _top_level_symbols(new_contents) 98 | if not what_to_import: # no need for a forwarding file 99 | print ('INFO: ignoring %s, it has no top-level symbols' % new_file) 100 | return 101 | 102 | import_lines = ['from %s import %s # NoQA: E501,F401(unused import)' 103 | % (_module_for_file(new_file), symbol) 104 | for symbol in what_to_import] 105 | 106 | # In the process of moving things out of this directory, the 107 | # subdirectory the old module came from may have been deleted. 108 | dir_parts = os.path.dirname(outfile).split(os.sep) 109 | for i in xrange(1, len(dir_parts) + 1): 110 | dir = os.path.join(*dir_parts[:i]) 111 | if not dir: 112 | continue 113 | try: 114 | os.mkdir(dir) 115 | except OSError: # probably directory already exists 116 | pass 117 | open(os.path.join(dir, '__init__.py'), 'a').close() 118 | 119 | with open(outfile, "w") as f: 120 | f.write(_PICKLE_LOGGER_CONTENTS % '\n'.join(import_lines)) 121 | 122 | 123 | def _parse_file_move(file_move): 124 | """Return the (old_filename, new_filename) tuple for a file move.""" 125 | _, old_filename, new_filename = file_move.split() 126 | return (old_filename, new_filename) 127 | 128 | 129 | def main(compare_with): 130 | cmd = [ 131 | 'git', 'diff', '-M', '--name-status', '--diff-filter=R', compare_with 132 | ] 133 | 134 | # Run the `git diff` command to list all file moves since a revision 135 | try: 136 | file_moves = subprocess.check_output(cmd) 137 | except subprocess.CalledProcessError: 138 | sys.exit("Could not diff against revision %s\n" % compare_with) 139 | 140 | # Try to make a pickle logger for each file move 141 | for line in file_moves.splitlines(): 142 | old_file, new_file = _parse_file_move(line) 143 | if _should_write_pickle_logger(old_file, new_file): 144 | _write_pickle_logger(old_file, new_file) 145 | 146 | 147 | if __name__ == '__main__': 148 | import argparse 149 | parser = argparse.ArgumentParser() 150 | parser.add_argument("-r", "--revision", default="HEAD", 151 | help="The revision you want to compare against.") 152 | args = parser.parse_args() 153 | 154 | main(args.revision) 155 | -------------------------------------------------------------------------------- /src/supporting-files/pickle_util.py: -------------------------------------------------------------------------------- 1 | """A wrapper around pickle and cPickle that support symbol renaming. 2 | 3 | Sometimes classes, class instances, functions, or other symbols are 4 | pickled. When we rename those symbols -- even by just moving them to 5 | another file -- then the pickled data cannot be unpickled anymore, 6 | since the unpickler can no longer find the symbol where it expects. 7 | 8 | To fix it, we keep a map in this file of oldname->newname. Then, 9 | whenever we unpickle an object and see oldname, we can instantiate a 10 | newname instead. 11 | 12 | We also add a few more optimizations: we use cPickle rather than 13 | pickle whenever we can, and when pickling we default to protocol 2 14 | rather than the inefficient protocol 0. 15 | 16 | Note this code has only been tested on python2. 17 | """ 18 | 19 | import cPickle 20 | import cStringIO 21 | import logging 22 | import pickle 23 | import sys 24 | 25 | 26 | # Every time you move path.to.module.symbol to some.other.path.symbol2, 27 | # add an entry like this to this dict: 28 | # ('path.to.module', 'symbol'): ('some.other.path', 'symbol2') 29 | # If you then move it *again* to 'a.third.location.symbol3', replace the 30 | # entry above with these two entries: 31 | # ('path.to.module', 'symbol'): ('a.third.location', 'symbol3') 32 | # ('some.other.path', 'symbol2'): ('a.third.location', 'symbol3') 33 | _SYMBOL_RENAME_MAP = { 34 | ('users.info', '_update_users'): ('accounts.info', '_update_users'), 35 | ('compat_key', '_CompatKey'): ('lib.compat_key', 'CompatKey'), 36 | ('compat_key', 'CompatKey'): ('lib.compat_key', 'CompatKey'), 37 | } 38 | 39 | 40 | def _renamed_symbol_loader(module_name, symbol_name): 41 | """Return a symbol object for symbol symbol_name, loaded from module_name. 42 | 43 | The trick here is we look in _SYMBOL_RENAME_MAP before doing 44 | the loading. So even if the symbol has moved to a different module 45 | since when this pickled object was created, we can still load it. 46 | """ 47 | (actual_module_name, actual_symbol_name) = _SYMBOL_RENAME_MAP.get( 48 | (module_name, symbol_name), # key to the map 49 | (module_name, symbol_name)) # what to return if the key isn't found 50 | 51 | # This is taken from pickle.py:Unpickler.find_symbol() 52 | try: 53 | __import__(actual_module_name) # import the module if necessary 54 | except ImportError: 55 | logging.error("Unable to import %s for %s", module_name, symbol_name) 56 | raise 57 | module = sys.modules[actual_module_name] 58 | return getattr(module, actual_symbol_name) 59 | 60 | 61 | def Unpickler(fileobj): 62 | """Like cPickle.Unpickler, but with our symbol-renamer. 63 | 64 | Note that like cPickle.Unpickler, this is not actually a class and 65 | you therefore can't subclass it. It also doesn't allow us to load 66 | classes that changed from new- to old-style, so if you need that, 67 | see StyleChangeUnpickler below. 68 | """ 69 | # With cPickle, to override how global-lookup is done, you just define 70 | # find_global. See the docs for details: 71 | # https://docs.python.org/2/library/pickle.html#subclassing-unpicklers 72 | unpickler = cPickle.Unpickler(fileobj) 73 | unpickler.find_global = _renamed_symbol_loader 74 | return unpickler 75 | 76 | 77 | class StyleChangeUnpickler(pickle.Unpickler): 78 | """Like pickle.Unpickler, but with our symbol-renamer and NEWOBJ hack. 79 | 80 | Like Unpickler, above, this uses our symbol-renamer. Unlike Unpickler, it 81 | also adds a hack to allow loading an old-style class that was pickled as 82 | new-style. (Python handles the other direction just fine.) It's necessary 83 | because some App Engine classes are new-style in Standard but old-style in 84 | python-compat and dev. Note that we can only implement this hack against 85 | pickle's Unpickler, not cPickle's, so we try that one first and only use 86 | this one if it fails. 87 | """ 88 | # With pickle, we have to override load_global, again see the docs: 89 | # https://docs.python.org/2/library/pickle.html#subclassing-unpicklers 90 | def load_global(self): 91 | module = self.readline()[:-1] 92 | name = self.readline()[:-1] 93 | self.append(_renamed_symbol_loader(module, name)) 94 | 95 | # Not in the documentation, but what the source code requires 96 | pickle.Unpickler.dispatch[pickle.GLOBAL] = load_global 97 | 98 | # This lets us support the new-style-to-old-style hack. 99 | # Note it depends on the pickle protocol being >=2, and using pickle, not 100 | # cPickle, to unpickle. VERY FRAGILE! 101 | def load_newobj(self): 102 | args = self.stack.pop() 103 | cls = self.stack[-1] 104 | try: 105 | obj = cls.__new__(cls, *args) 106 | self.stack[-1] = obj 107 | except AttributeError: # cls is actually an old-style class 108 | k = len(self.stack) - 1 # point to the markobject 109 | self.stack.extend(args) 110 | self._instantiate(cls, k) 111 | pickle.Unpickler.dispatch[pickle.NEWOBJ] = load_newobj 112 | 113 | 114 | def dumps(obj, protocol=cPickle.HIGHEST_PROTOCOL): 115 | """Return a pickled string of obj: equivalent to pickle.dumps(obj).""" 116 | try: 117 | return cPickle.dumps(obj, protocol) 118 | except Exception: 119 | logging.error("Unable to pickle '%s'", obj) 120 | raise 121 | 122 | 123 | def loads(s): 124 | """Return an unpickled object from s: equivalent to pickle.loads(s).""" 125 | unpickler = Unpickler(cStringIO.StringIO(s)) 126 | try: 127 | try: 128 | return unpickler.load() 129 | except cPickle.UnpicklingError: 130 | # We may have hit the NEWOBJ problem. Try again using 131 | # NewobjSafeUnpickler in that case. 132 | unpickler = StyleChangeUnpickler(cStringIO.StringIO(s)) 133 | return unpickler.load() 134 | except Exception: 135 | logging.error("Unable to unpickle '%s'", s) 136 | raise 137 | -------------------------------------------------------------------------------- /src/posts/khanalytics.md: -------------------------------------------------------------------------------- 1 | title: "New data pipeline management platform at Khan Academy" 2 | published_on: April 30, 2018 3 | author: Ragini Gupta 4 | team: Infrastructure 5 | ... 6 | 7 | Data is very crucial to Khan Academy and is itself an internal product for the company. Analysts, engineers and marketing are some of the daily consumers of data. We have various systems for managing pipelines which are self-contained processes for doing analysis on some data. They consume some input and produce some output. There are more than two hundred data pipelines in the company currently and the number is constantly growing. 8 | 9 | We at Khan Academy realized the importance of having an efficient way of managing our increasing number of pipelines and the result was… Khanalytics. 10 | 11 | ## Life before Khanalytics 12 | 13 | There was **no single place** where one could find all pipelines: 14 | 15 | * Some were very well tied to our website and resided in our cloud infrastructure along with the rest of our application. 16 | * Some pipelines were more manual and generated data by manual querying. 17 | * Some were using R scripts and were run manually on local machines. 18 | * Some were hosted on a separate machine and run using a cron service. 19 | 20 | We had complex pipelines with **different stages that couldn’t talk to each other**: 21 | 22 | * Some pipelines were complex enough to have their different stages written in different languages or use different tools. 23 | * There was no way we could use R, Python, Google BigQuery, Google Cloud Dataflow etc. in the very same pipeline and pass data between those stages. 24 | 25 | The **pipeline iteration process was slow**: 26 | 27 | * Since most pipelines lived with the code of rest of the website, changing an existing pipeline or creating a new one required many of the same steps as building user-facing functionality. 28 | * Scheduling a single pipeline also involved multiple steps every time. 29 | * Debugging pipeline failures was hard as the logs were either not easy to find or not informative enough. 30 | 31 | **It’s too easy to introduce bugs**: 32 | 33 | * Since most pipelines existed with the website codebase, a bug in the pipelines’ code could introduce errors on the complete website. 34 | 35 | ## Khanalytics: a platform for data pipeline lifecycle management 36 | 37 | It’s easier to describe Khanalytics by talking of its features: 38 | 39 | * Completely sandboxed environment for running batch jobs. 40 | * A web user interface that’s user friendly and aimed to be used by non-developers as well. 41 | * Ability to parallelize different steps in a pipeline automatically. 42 | * Single place to find all logs for debugging. 43 | * Ability to schedule pipelines individually and also with dependency on other pipelines. 44 | 45 | ## Architecture 46 | 47 | Khanalytics is built on some core fundamentals mentioned below. 48 | 49 | **Everything is a container**: All steps in Khanalytics (called stages) including the core components of the application are completely isolated from each other since they run in containers. We use [Kubernetes](https://kubernetes.io/) in Google Kubernetes Engine to create a cluster and manage deployment of our containers. We pre-build images for all containers (Python, BigQuery, R, etc.) and start a container with the relevant image. Since everything is in a container, it’s easy enough to customize or extend the types of pipelines we’d want to support. 50 | 51 | **Statelessness**: All state is managed by [etcd](https://coreos.com/etcd/), which is a state store. The individual services communicate with etcd about the state of individual pipelines, to start any new pipelines or to update the current state of a running pipeline. This improves reliability of the application as there are fewer communication paths. 52 | 53 | **Static configuration**: We store the configuration of a pipeline in a JSON format which is static. The configuration consists of the environment, individual stages, inputs, outputs and intermediates. We allow variable interpolation in the environment variables so that certain things like current date that are dynamic, are filled in at runtime. The static configuration allows us to lint it as soon as it’s created ensuring that the configuration is always valid. It also allows us to know about the pipeline in advance and how parallel its different stages are. 54 | 55 | **Scheduling**: Khanalytics uses the Kubernetes scheduler to schedule jobs and run them at a specified time, once or repeatedly. This is done with Kubernetes’ inherent cron feature called [CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/). 56 | 57 | **Permissions**: Khanalytics uses [Google’s Identity Aware Proxy (IAP)](https://cloud.google.com/iap/). Once a user accessing Khanalytics is authorized, they log into the application using a service account which in turn has pre-declared access to different external services which can be accessed from Khanalytics. 58 | 59 | ## User-interface 60 | 61 | While we can access and manage pipelines using a command-line tool, a user friendly UI allows for non-developer users to create, manage and view their pipelines. 62 | 63 | ![A picture of the list of completed pipelines](/images/khanalytics/completed-pipelines.png) 64 | 65 | ![A picture of the logs of a pipeline](/images/khanalytics/pipeline-logs.png) 66 | 67 | ## Impact 68 | 69 | Khanalytics started as a hackathon project at Khan Academy and resulted in being our single, go-to place for managing analytics pipelines. Apart from solving the problems it was created for, we find it creating bigger impacts. It provides more empowerment to the non-developers which encourages them to create data pipelines for things that weren’t priorities for the engineering team. We saw an increase in its usage and a reduction in debugging and monitoring time. It has also promoted reusability of data or pipeline stages as a result of more visibility and easier interpretability. 70 | 71 | ## What next? 72 | 73 | We are improving the platform significantly in its reliability, usability as well as its ability to meet all needs of Khan Academy. We have a lot of work ahead of us in migrating all our analytics pipelines to Khanalytics and this migration process is also an input into testing and improving Khanalytics even more. We don’t think that we are far away from Khanalytics being the single solution to data pipelining at Khan Academy or even beyond, as we consider the possibility of open sourcing Khanalytics for others to use. 74 | 75 | *Many thanks to Colin Fuller, Kevin Dangoor and Tom Yedwab for their review of this post and a big shout-out to all who've worked and provided feedback on Khanalytics to help make what it is today.* 76 | -------------------------------------------------------------------------------- /src/posts/tips-for-code-reviews.md: -------------------------------------------------------------------------------- 1 | title: Tips for giving your first code reviews 2 | published_on: September 18, 2017 3 | author: Hannah Blumberg 4 | team: Mobile 5 | ... 6 | 7 | At Khan Academy, (nearly) every piece of code that goes into our codebase has been reviewed by at least one other person. Code reviews help us keep code maintainable and clean, catch big-picture issues early, build a shared understanding of the codebase, and socialize new engineers. To learn more about our code review beliefs and practices, check out some oft-cited blog posts [here](http://bjk5.com/post/3994859683/code-reviews-as-relationship-builders-a-few-tips) and [here](https://www.arguingwithalgorithms.com/posts/13-03-14-code-reviews). 8 | 9 | When I joined Khan Academy as an intern, I read a ton of documents describing the purpose, importance, and process of code reviews. Since I was a college student at the time and was accustomed to receiving feedback from professors, I felt prepared to have my code reviewed. I did not feel prepared, however, to begin reviewing code myself. 10 | 11 | Reviewing other people's code – especially code written by smart, experienced engineers – can be really intimidating. *What could I, an intern with just a few computer science courses under my belt, contribute?* 12 | 13 | Now that I have completed my internship and have been full time at Khan Academy for over a year, I know that everyone – even the newest of interns – can add value to the code review process. 14 | 15 | To help you get started, here are some concrete suggestions for reviewing code for the first time: 16 | 17 | --- 18 | 19 | ## 1. Ask questions 20 | 21 | This is probably my favorite piece of advice for getting started with code reviews. Code review comments do not have to be direct requests for changes. You might ask the author to describe the trade-offs they considered, explain how a piece of code works, or provide more context on the project. 22 | 23 | Asking questions gives you the opportunity to learn and gives the author the opportunity to articulate, clarify, or challenge their thinking. It may also help the author identify areas of the code that are less clear and consequently harder to maintain. 24 | 25 | ![Example of asking questions](/images/tips-for-code-reviews/ask-q-1.png) 26 | 27 | If you find a mistake or identify an area of improvement while doing a code review, you should absolutely feel empowered to share that directly. It's easy, though, to second guess yourself, especially when reviewing an experienced engineer's code. If you find yourself about to delete a code review comment, try rephrasing it as a question! Asking a question is often just as valuable to the author, and it may feel a little easier to post. 28 | 29 | ![Example of asking questions](/images/tips-for-code-reviews/ask-q-2.png) 30 | 31 | ## 2. Try pair reviewing 32 | 33 | This is a tip I learned from one of our engineering managers, Celia La. Ask your mentor or onboarding buddy (or anyone else on your team) if they would be willing to do a "pair review" session. You and your teammate can sit together or screen share as you work through a code review together. 34 | 35 | By watching and asking questions, you can learn more about your teammate's process and techniques. 36 | 37 | If you are not able to set up a pair review session, you can also look at code reviews that your teammates have done for one another. Although you may not learn your teammates' processes, you will still get a sense of the type of feedback they give one another. 38 | 39 | ## 3. Review code in your IDE 40 | 41 | This tip comes from former engineering lead and my onboarding buddy, Charlie Marsh. In Charlie's [blog post](http://www.crmarsh.com/code-review/), he describes the benefits of reviewing code in your own integrated development environment (IDE). 42 | 43 | At Khan Academy, we typically review code through [Phabricator](https://secure.phabricator.com/)'s web interface. The [`arc patch`](https://secure.phabricator.com/book/phabricator/article/arcanist/) command allows you to apply the changes in a code review to your local copy of the codebase. From there, you can explore the code in your own IDE. 44 | 45 | As Charlie describes, reviewing code in your IDE allows you to put yourself "in the author's shoes" and catch things you might not see in the web interface. You can navigate more naturally between files and see changes in the context of the codebase. 46 | 47 | Reviewing code in an IDE also allows you to try out ideas before you share them so that you can suggest changes with more confidence. 48 | 49 | ##4. Comment on anything 50 | 51 | When you are doing your first few code reviews, you should feel free to comment on anything. 52 | 53 | For example, you should feel comfortable pointing out small fixes that are not blocking. Although you will likely move away from these comments over time, they can help you build confidence. 54 | 55 | If you leave "nit" (short for nitpick) level comments, it is best to distinguish them from more crucial fixes. I generally like to add the word "nit" before these comments to communicate to the author that they are not blocking. 56 | 57 | ![Example nit comment](/images/tips-for-code-reviews/comment-on-anything-1.png) 58 | 59 | Borrowing from Ben Kamens' [blog post](http://bjk5.com/post/3994859683/code-reviews-as-relationship-builders-a-few-tips), you should also feel free to "point out the good stuff" in your comments. You might thank the author for a really helpful comment, call out a clever (but clear!) technique, or compliment the thoroughness of their testing. These comments encourage the author to continue doing the "good stuff" and help to build relationships. 60 | 61 | ![Example compliment](/images/tips-for-code-reviews/comment-on-anything-2.png) 62 | 63 | ![Example compliment](/images/tips-for-code-reviews/comment-on-anything-3.png) 64 | 65 | ## 5. Review a peer's code 66 | 67 | Even if you are a very new or junior engineer, you can add value to any code review. That being said, it can be extra intimidating to review code written by experienced engineers. To become more comfortable and build your confidence, try finding someone who you consider a peer (maybe a fellow intern or someone who started around the same time as you) and ask if they would be interested in having their code reviewed by you and vice versa. 68 | 69 | Reviewing a peer's code can be a more comfortable introduction to code reviews. It will also give you extra opportunities to practice! Keep in mind that you do not need to be the sole reviewer of your peer's code; you can leave comments without approving or rejecting the change. 70 | 71 | --- 72 | 73 | Reviewing code is a skill that develops over time. The first step to improving is to just get started! 74 | 75 | -------------------------------------------------------------------------------- /src/posts/career-development.md: -------------------------------------------------------------------------------- 1 | title: "Engineering career development at Khan Academy" 2 | published_on: April 11, 2016 3 | author: Ben Eater 4 | team: Eng Leads 5 | ... 6 | 7 | At Khan Academy, we see ourselves as part of a broader engineering community and just as we aim to open-source much of what we build, we also want to share what we learn while growing and maturing as an engineering team. We’ve previously shared our [Engineering Principles](https://docs.google.com/document/d/1PW4NYn9pYNam2EuGEsTN9pTgwTfFnT_R9OZLJJICWQU/edit). Today we’re releasing our [Engineering Career Development](https://docs.google.com/document/d/1qr0d05X5-AsyDYqKRCfgGGcWSshTMd_vfTggfhDpbls/edit) guide. 8 | 9 | We want to share this with the community in the hopes that other engineering teams may find value and give us feedback, just as we’ve used/stolen/gained so much value from [Fog Creek](http://joelonsoftware.com/articles/ladder.html), [Stack Exchange](https://blog.stackoverflow.com/2011/07/how-much-should-you-pay-developers/), [Rent the Runway](http://dresscode.renttherunway.com/blog/ladder), and others in making our career structures stronger. 10 | 11 | Why do we care so much about this stuff? 12 | 13 | ### Focus on the learner 14 | 15 | Like many companies, at Khan Academy we have a list of company values that drive what we do and how we do it. Topping our list is “Focus on the learner.” Perhaps that seems obvious—who else would we be building Khan Academy for, if not the learners who use it? We’re a non-profit, so it’s not shareholders—we don’t have any. But much of education (non-profit or otherwise) is focused on every stakeholder but the learner. It’s important we don’t lose sight of our goal of making the education system better for more than just parents, teachers, or a small set of privileged students. 16 | 17 | ### Keep learning 18 | 19 | Only as continuous learners ourselves can we really empathize with and understand the mind of our learners. Consequently, our second company value is “Keep learning.” Not surprisingly, there’s no lack of opportunity to learn at Khan Academy. Apart from all the amazing content we produce for our learners, we love to teach each other too. We’ve had fellow team members teach us everything from bookbinding to cooking to coffee roasting to zumba. But the one thing everyone on our engineering team is continually learning is how to become better engineers. 20 | 21 | As an engineering manager, I see it as a primary part of my job to support the other people on my team through their individual learning journeys. It’s pretty neat that by working on that challenge, we get to improve ourselves and simultaneously build even better empathy for our users. Our career development guide is a small piece of that puzzle. 22 | 23 | But it took us a while to get here. Our career development guide started the way most of these career ladders do—and for similar reasons: We needed some sort of framework for figuring out how to hire and pay people with different skillsets and impact to the mission, we wanted it to be fair, and we needed to be able to talk about performance and growth. So we copied a lot of stuff from Fog Creek’s professional ladder and tweaked it a bit to meet our needs. That first version served us reasonably well for about 3 years, but over time we discovered its limitations. 24 | 25 | The original version described each level with a single paragraph, making it hard for someone to really know what it took to move from one level to the next. As a manager, I had a shared understanding with other managers about what each level meant (or at least I assumed I did) and could give individual guidance to people. But that’s not good enough. It’s not good enough for me to be the only one to understand the full career progression and just tell you what the next step is—even if I’m right (which is far from guaranteed). 26 | 27 | ### Student agency 28 | 29 | As we were learning that lesson, it was teaching us the parallel lesson that even if we built the world’s best personalized learning system with all the fanciest artificial intelligence that always gave perfect recommendations to each of our students, it wouldn’t be good enough. 30 | 31 | It turns out that true personalized learning requires more. It requires students to fully understand the path that they’re on. It requires students to know where they are, where they’re going, and what steps stand in the middle. Ideally, armed with this understanding, students have the agency to choose their own path. The teacher’s role is to help students understand and internalize the context, motivate them, reassure them, and give feedback along the way. But it’s the student who’s really driving things. 32 | 33 | Changing the broader education system is going to take a bit more work, but this seemed like something we could try for our engineering team pretty quickly. So about six months ago, we added a ton of additional clarity to our engineering career development guide. We wanted everyone to have a shared understanding of how you grow as an engineer. For example, what skills does it take to become an engineering lead for a major initiative? If that’s still a few years away and you have trouble ever imagining yourself as a lead, what can you work on now to take a step towards being able to imagine that? We wanted the shared ~understanding that we had as managers to actually be shared by every engineer in the organization. We may never fully meet that goal, but we’ve made a lot of progress. 34 | 35 | As a manager, I now find myself in a lot more of the kinds of conversations I want to be in. I’m having a lot fewer prescriptive conversations—e.g., “here’s the next thing I think you should work on”—and more conversations where I’m brainstorming possible career arcs with someone who knows how to take the next step and feels empowered to do so. 36 | 37 | ### Shipping beats perfection 38 | 39 | We know this document is still far from perfect. We like to look at the way our team functions the same way we look at everything else we make: Something we constantly iterate on. I’m hopeful that by sharing this (far from perfect) document, we’ll get some feedback that helps us learn from others outside Khan Academy. 40 | 41 | * [Engineering Principles](https://docs.google.com/document/d/1PW4NYn9pYNam2EuGEsTN9pTgwTfFnT_R9OZLJJICWQU/edit) 42 | * [Engineering Career Development](https://docs.google.com/document/d/1qr0d05X5-AsyDYqKRCfgGGcWSshTMd_vfTggfhDpbls/edit) 43 | 44 | Feel free to fork our guide and modify it to use it in your own organization. If you do, please [reach out to me](mailto:eater@khanacademy.org) and share what you learned! Or if you don’t want to bother implementing this in your own organization, you could just join on our mission to provide a free, world-class education for anyone, anywhere. [We’re hiring](https://www.khanacademy.org/careers)! :) 45 | 46 | Thanks again to the many shoulders we stood on: 47 | 48 | * [Fog Creek](http://joelonsoftware.com/articles/ladder.html) 49 | * [Stack Exchange](https://blog.stackoverflow.com/2011/07/how-much-should-you-pay-developers/) ([pdf](https://github.com/StackExchange/stack-blog/files/60428/Stack-Exchange-Developer-Compensation.pdf)) 50 | * [Rent the Runway](http://dresscode.renttherunway.com/blog/ladder) 51 | 52 | -------------------------------------------------------------------------------- /src/posts/starting-android.md: -------------------------------------------------------------------------------- 1 | title: "Starting Android at Khan Academy" 2 | published_on: February 29, 2016 3 | author: Ben Komalo 4 | team: Mobile 5 | ... 6 | 7 | ## The journey of a thousand miles... 8 | 9 | In March, 2015—almost 1 year ago to the day—we started developing our first Android app at Khan Academy. 10 | 11 | ![first commit](/images/starting-android/first-android-commit.png) 12 | *A single step* 13 | 14 | By then, Android was on version 5.0 Lollipop and nearing [1 billion active monthly users](http://www.cnet.com/news/google-io-by-the-numbers-1b-android-users-900m-on-gmail/), representing around 8 out of every 10 phones worldwide. Our mission is to provide a free world-class education for anyone, anywhere, so we had to provide a great learning experience on this platform: Android reaches billions of users, many of whom rely on their phone as their only computing device. 15 | 16 | 17 | We launched version 1.0 of our app on the [Play Store](https://play.google.com/store/apps/details?id=org.khanacademy.android) in August 2015, having learned a ton about Android development as a team. There’s plenty more ahead, but we thought now would be a good time to share a bit of our journey. 18 | 19 | ## Libraries and foundations 20 | 21 | Ironically, being late to the Android game came with its advantages. The platform had evolved significantly by early 2015, and we were able to target API level 16 and higher while still reaching 95% of users. Furthermore, the [Support Library](http://developer.android.com/tools/support-library/index.html) had provided fantastic new utilities like the `RecyclerView` and `CoordinatorLayout`, making modern Android “Material UI” significantly easier to build. The vibrant open-source community had also gifted us with plenty of useful libraries like [Retrofit](http://square.github.io/retrofit/) and [Picasso](http://square.github.io/picasso/), which we weren’t shy about adopting. 22 | 23 | Of course, good utility libraries are not enough: we knew we needed strong foundations throughout our code. We enforced separation of concerns right off the bat with two separate modules in our app: an Android-agnostic “core” module, and an “app” module for Android-specific application code: 24 | 25 | Our core module deals with tasks like fetching data, storing data, and transforming that data, while our app module reads that data, shows it to the user, and writes changes. Besides keeping our code clearer and more organized, this separation delivered a variety of secondary benefits. For instance, we can run our core module on a vanilla JVM without any Android runtime libraries or special mocking, making tests easier to write and also significantly faster to run – fast enough that we can run them as a pre-commit check before sending out a change revision: 26 | 27 | ![test run](/images/starting-android/core-tests.png) 28 | *1290 tests in ~3s ⚡️* 29 | 30 | Those core tests can run on our CI server without having to deal with emulators, which have proven to be difficult to maintain. This has made us more confident in our tests, instilled a test-heavy culture in our development team, and allowed us to run more extensive end-to-end tests pretty easily. For example, one test tries to download and process our content library; we run this continuously to warn the content and server teams in case they accidentally change an API in a backwards-incompatible way. 31 | 32 | Modules add some complexity to the build setup, but the relatively small cost has been well worth paying. We’ve since added a few more small modules, though the broader separation of “core” from “app” delivered the most benefit. In the future, as we consider cross-platform code sharing solutions, we may adapt the core module to be shared across our iOS and Android apps (more on that below). 33 | 34 | ## Tools 35 | 36 | In addition to having solid architectural foundations, we wanted to make the surrounding developer experience great. We had invested in [various](http://engineering.khanacademy.org/posts/tota11y.htm) [kinds](http://engineering.khanacademy.org/posts/i18nize-templates.htm) [of tools](http://engineering.khanacademy.org/posts/i18n-babel-plugin.htm) for our web app infrastructure, and we wanted to sharpen our tools on Android too. 37 | 38 | We’re still far from the ideal scenario: deploying and testing UI changes end-to-end still takes too long. But tools like [Facebook’s Stetho](http://facebook.github.io/stetho/) provide a great debugging experience that can save lots of time once the app is on a device. All our network requests use [OkHttp](https://github.com/square/okhttp), and [with the right configuration](http://facebook.github.io/stetho/#integrations), Stetho can inspect them. 39 | 40 | We’ve also created a custom `DumperPlugin`, which runs arbitrary commands in our app via the command line. Here we force our app to sync new content from the server: 41 | 42 | ``` 43 | $ dumpapp ka-debug update-topic-tree 44 | Kicked off service to update topic tree 45 | ``` 46 | 47 | For automating releases, we use [Triple-T’s Gradle plugin](https://github.com/Triple-T/gradle-play-publisher) to upload APKs to the Play Store. Our CI server publishes a new build from `master` every night to our alpha channel (internal employees). We use Git tags to track the version code; each build increments the number and makes a new tag (though we bump the marketing-oriented “version name” manually). 48 | 49 | We’re still building out lots of tooling to make things easier, including our still-nascent linting tools. If you’ve got favorites to share, we’d love to chat. 50 | 51 | ## Many miles ahead 52 | 53 | Our team has learned a lot in the last year. Not only have we come a long way in building out our technical capabilities, but our fantastic design team has also adapted to thinking about new features in a holistic fashion; all three platforms (web, iOS and Android) are considered up front and designs are typically done simultaneously so the learning experience can make sense regardless of how you access it. 54 | 55 | That being said, we’re still a fairly small team, and re-building a feature three times is costly. We’re actively exploring possibilities for [mobile code sharing strategies](https://docs.google.com/document/d/1zEBxHsbXaKlvzwYxzoElkF8K8rZ0vaXmiWoLUtsd0Tg/edit#), but have yet to find a satisfactory solution. There’s also much to learn as a broader team about coordinating work across three platforms. Should we build features on one platform before starting work on the rest, for validation’s sake? How should we best enable our content creators to make compelling experiences that are flexible and can adapt to the learner’s device? 56 | 57 | Despite the challenges ahead, we’re excited to be building on Android. More importantly, we’re excited about the possibility of reaching vast numbers of learners through the platform. There’s a lot more we’re working on, like bringing the interactive exercises and other features learners have enjoyed on our website and in our iOS app to Android. We’re also thinking about ways to deliver our content in extremely limited connectivity environments, and other situations which can benefit from a native Android app. Excited about empowering learners using mobile technology? [Come join us](https://www.khanacademy.org/careers). 58 | -------------------------------------------------------------------------------- /src/posts/snippet-server.md: -------------------------------------------------------------------------------- 1 | title: "The weekly snippet-server: open-sourced" 2 | published_on: February 1, 2016 3 | author: Craig Silverstein 4 | team: Infrastructure 5 | ... 6 | 7 | 8 | When I joined Khan Academy, my first project was to write a version of 9 | the weekly-snippet server I had worked with [at 10 | Google](http://blog.idonethis.com/google-snippets-internal-tool/). 11 | Years later, with the help of many intrepid Khan Academy employees 12 | ([one such employee](http://michelleontheweb.com/), [another such 13 | employee](http://rileyjshaw.com/), [a third such 14 | employee](https://bitquabit.com/), [you get the 15 | idea](http://mroth.info/)), it's [*ready for the 16 | world*](http://www.github.com/Khan/snippets)! 17 | 18 | While there are [many](https://weekdone.com/) 19 | [snippet](https://www.workingon.co/) 20 | [systems](https://www.teamsnippets.com/) out there, this one is 21 | optimized for simplicity (also, free-ness). For instance, it prefers 22 | single webpages with lots of info over paging, queries, or fancy 23 | JavaScript. Filling out a snippet involves writing into a textbox: no 24 | fields or text editors or other barriers to productivity. (Markdown 25 | is available for those who want nice formatting.) This makes it easy 26 | to learn and easy to program against. 27 | 28 | 29 | What are weekly snippets? 30 | ------------------------- 31 | 32 | A weekly snippet is an (ideally) brief description of what you did the 33 | last week. What is brief? The snippet-entry textbox is 34 | sized for 4 bullet-point entries, each 80 characters or less: 35 | 36 | ![Snippet-entry page](/images/snippets-user-2.png) 37 | 38 | Your snippets are visible to everyone else on your email domain. (So 39 | my snippets are visible to everyone who logs in to KA snippet server 40 | with a `@khanacademy.org` email address.) Depending on your 41 | configuration options, they may also be visible to everyone else on 42 | your server. 43 | 44 | ![Snippet-view page](/images/snippets-weekly-2.png) 45 | 46 | (You may notice, on this page, a bunch of people have not entered 47 | snippets. At Khan, that's perfectly ok. This is a tool for people to 48 | use if they find it useful, and ignore if they don't.) 49 | 50 | 51 | Why have snippets? 52 | ------------------ 53 | 54 | Different people might use weekly snippets for different purposes: 55 | 56 | * Instead of a weekly standup or other meeting where everyone shares 57 | what they've done in the last week, they can just read (and write) 58 | snippets. 59 | * Managers can read snippets of their direct reports to make better 60 | use of 1-on-1 meetings. 61 | * You can look over your own snippets when writing a self-evaluation 62 | or applying for a promotion, or when you have any other need to remind 63 | yourself what you've worked on. 64 | 65 | I've found this last reason is particularly compelling. I also use 66 | snippets as a simple "time and motion" study: when I have too many 67 | things to put into snippets one week, I know I'm being spread too 68 | thin! 69 | 70 | Another benefit of snippets is serendipitous helping: by reading 71 | someone's snippet, you may discover a task or problem they're working 72 | on that you can help with, that otherwise you would never have known 73 | about. 74 | 75 | 76 | What are snippets not good for? 77 | ------------------------------- 78 | 79 | Some people go into a snippet system with unrealistic expectations and 80 | are disappointed. 81 | 82 | * Snippets do not work well for large groups, say **over 100 83 | people**. If you have 1000 people using your snippet server, it is 84 | neither practical nor useful to read through everyone's snippets 85 | every week. 86 | 87 | * Snippets are, by design, a low level tool: they show you trees but 88 | not the forest. The snippet system does not support "rolling up" 89 | groups of snippets or having team-based snippets (though certain 90 | individuals could certainly choose to have their own snippets refer 91 | to a team's progress). 92 | 93 | * Snippets do not provide context. If you don't already know what 94 | someone is working on, their snippet may enlighten you, but it will 95 | just as likely confuse you. 96 | 97 | At Khan Academy, the entire company uses one snippet server. The 98 | snippets are divided into various categories, some functional, some 99 | project-based. I like to skim over the snippets for people in 100 | unrelated categories such as "facilities" or "recruiting." I read 101 | more closely the snippets in projects I'm interested in but not 102 | working on, such as "mobile." And I read most closely the snippets of 103 | people in my own project or closely related projects. 104 | 105 | 106 | How do you use the snippet-server? 107 | ================================== 108 | 109 | After setting up your settings, to control things like how public your 110 | snippets are and whether you want to use plain text or 111 | [markdown](https://daringfireball.net/projects/markdown/), there are 112 | only two web pages: the one where you write your snippets, and the one 113 | where you read everyone's snippets for a week. 114 | 115 | The administrator can set up the system to send you reminder emails to 116 | write snippets, or to email when snippets are ready for a week. (The 117 | snippet server can also use chat systems for this.) 118 | 119 | 120 | The Google connection 121 | --------------------- 122 | 123 | The snippet server is built on top of [Google 124 | AppEngine](https://cloud.google.com/appengine/docs), and uses Google 125 | services for authentication. To use it, you need to clone the 126 | [snippet github project](https://github.com/Khan/snippets) and then 127 | upload it to your own appengine instance. (It uses few resources, so 128 | Google's "free tier" would work fine.) 129 | 130 | The people using your snippet server must log in using Google (aka 131 | Gmail) accounts. The snippet server works particularly well with 132 | companies that use [Google Apps for Work](https://apps.google.com). 133 | 134 | 135 | Email and chat 136 | -------------- 137 | 138 | The snippet server integrates with email, HipChat, and Slack. 139 | 140 | It can send individual emails to people who have not written a snippet 141 | for this week, reminding them to do so. (Users can turn this feature 142 | off in their preferences.) It can also send an email to all 143 | registered users, at 5pm on Monday, to say snippets are ready. 144 | 145 | It can also send reminders and ready messages via chat. (In this 146 | case, the reminder isn't 147 | [[yet]](https://github.com/Khan/snippets/issues/18) individualized.) 148 | 149 | 150 | Try it out! 151 | =========== 152 | 153 | The Snippet Server has been developed in the open since day one, but 154 | it hasn't been advertised that well. Now we're looking to change 155 | that. Contributors welcome! 156 | 157 | We believe that snippets can be a useful tool for a small to 158 | medium-sized team, and while there are several snippet server 159 | implementations out there, this one's ease of use and low low price 160 | makes it an appealing alternative. Try out the server and see what 161 | you think. 162 | 163 | -------------------------------------------------------------------------------- /src/posts/tota11y.rst: -------------------------------------------------------------------------------- 1 | title: tota11y - an accessibility visualization toolkit 2 | published_on: June 8, 2015 3 | author: Jordan Scales 4 | team: Web Frontend 5 | ... 6 | 7 | Today we're releasing `tota11y `_ (`on GitHub `_), an accessibility visualization 8 | toolkit that aims to reduce the friction of a11y testing. 9 | 10 | .. image:: /images/tota11y-logo.png 11 | :alt: tota11y logo 12 | 13 | Inspiration 14 | =========== 15 | 16 | Accessibility is hard for many reasons. While current tooling provides 17 | mechanisms for detecting most accessibility violations, there remains a 18 | certain amount of disconnect between the developer and the problems they are 19 | causing. Most of these errors are things we can't see, things that won't 20 | affect us, and things without a perfect, exact *fix*. 21 | 22 | tota11y aims to solve these problems by providing a fun, interactive way to 23 | **see** accessibility issues. Not only should the web be fully accessible to 24 | all, but developers should feel **empowered** to fix and prevent accessibility 25 | violations from happening in the first place. 26 | 27 | A bit of history 28 | ================ 29 | 30 | We've been explicitly working to improve the accessibility of `Khan Academy 31 | `_ since early January. In that time we've seen first 32 | hand what it takes to go through each and every page on our website and fix 33 | things that may prove to be troublesome to assistive technologies. 34 | 35 | `John `_ and I were both very new to this, so we set out and 36 | did our research, wrote some tests to detect violations using `Chrome's 37 | Accessibility Developer Tools `_, 38 | and got to work. 39 | 40 | A few weeks later we had fixed a significant chunk of accessibility errors on 41 | our site, and learned an immense amount about assistive technologies. 42 | 43 | .. image:: /images/a11y-devtools.png 44 | :alt: Chrome's Accessibility Developer Tools reporting some errors on our homepage 45 | :width: 100% 46 | 47 | Then the hard part came. 48 | 49 | *We* felt capable of fixing most accessibility violations on our site, but 50 | how could we spread that knowledge to *the team* efficiently? How could we make 51 | every `Khan Academy employee `_ feel 52 | empowered to report and fix accessibility violations? 53 | 54 | We gave talks, wrote docs, sent out emails, but regressions still popped up. 55 | Our tests ran, but were flaky, and didn't gain the same level of respect as our 56 | unit tests or linter. 57 | 58 | Simply put, our dev team still didn't fully understand the problems they were 59 | causing, and how to fix them. 60 | 61 | Meet tota11y 62 | ============ 63 | 64 | About a month ago we set out to build `tota11y `_ 65 | as an internal project for Khan Academy's "Web Frontend" team. 66 | 67 | The aim was to make it as simple as possible for developers to do manual 68 | accessibility testing as part of their normal work. Rather than requiring 69 | our dev team to dig through long-winded audit reports for violations they 70 | didn't understand, we wanted provide simple visualizations where they already 71 | were - the browser, right in front of them. 72 | 73 | So we started off with the idea of "annotations." We highlight parts of the 74 | current document, either to point out errors, successes, or just to label 75 | important tags like headings or `ARIA landmarks `_. 76 | 77 | .. image:: /images/early-tota11y.png 78 | :alt: An early tota11y demo showing heading annotations 79 | :width: 100% 80 | 81 | *A (very) early proof-of-concept for tota11y.* 82 | 83 | We ran with this core idea of "annotations" and expanded it, as you'll see, 84 | to include detailed error messages, suggestions for fixes, and more. 85 | 86 | What can tota11y do 87 | =================== 88 | 89 | tota11y is a `single JavaScript file `_ that you can include in your document like so: 90 | 91 | ```` 92 | 93 | Once you see the glasses in the bottom left corner of your window, you're good 94 | to go. 95 | 96 | .. image:: /images/tota11y-button.png 97 | :alt: The collapsed tota11y toolbar, a small button with a glasses icon 98 | :width: 100% 99 | 100 | tota11y currently includes `plugins `_ for 101 | the following: 102 | 103 | * detecting images with/without alt text (and presentation images) 104 | * labeling text with contrast violations (and suggesting appropriate color combinations) 105 | * outlining a document's heading structure and pointing out any errors with it 106 | * highlighting input fields without appropriate labels (and suggesting fixes based on context) 107 | * labeling all ARIA landmarks on the page 108 | * detecting unclear link text such as "Click here" and "More" 109 | 110 | Many of these come directly from `Google Chrome's Accessibility Developer Tools `_. 111 | 112 | .. image:: /images/tota11y-expanded.png 113 | :alt: The expanded tota11y toolbar displaying a list of plugins 114 | :width: 100% 115 | 116 | Some plugins (like the landmarks plugin) are as simple as labeling parts of the 117 | page. 118 | 119 | .. image:: /images/tota11y-wikipedia.png 120 | :alt: tota11y highlighting aria landmarks on wikipedia.org 121 | :width: 100% 122 | 123 | Others provide an extended summary of the page, like the headings plugin, using 124 | what's known as the "info panel." 125 | 126 | .. image:: /images/tota11y-wikipedia-headings.png 127 | :alt: tota11y highlighting heading tags and structure on wikipedia.org 128 | :width: 100% 129 | 130 | Also using this info panel, we can report errors in more detail and offer 131 | suggestions. 132 | 133 | .. image:: /images/tota11y-github-contrast.png 134 | :alt: tota11y explaining contrast violations and offering suggestions on github.com 135 | :width: 100% 136 | 137 | While we can't guarantee to solve all of your accessibility troubles, we think 138 | this approach makes violations easier to digest and will inspire developers to 139 | think differently about accessibility. 140 | 141 | What's in store? 142 | ================ 143 | 144 | We want to see how others use tota11y, and figure out what other sorts of 145 | accessibility violations we can help fix. Some ideas include: 146 | 147 | * proper/improper usage of the "tabindex" attribute 148 | * improper disabling of focus styling 149 | * buttons that are not keyboard-accessible 150 | 151 | We also want to continue building a solid API for tota11y, enabling developers 152 | to write their own tota11y plugins which may not be included in the original 153 | source. 154 | 155 | And we're planning on bundling tota11y as a series of browser extensions to 156 | make it easier to test websites without the need to include a script in your 157 | application. 158 | 159 | We hope using tota11y makes you feel empowered to spot, diagnose, and fix 160 | accessibility issues on your webpages. Be sure to `check us out on GitHub `_ and let us know how we can help. 161 | -------------------------------------------------------------------------------- /src/posts/architects-at-khan.md: -------------------------------------------------------------------------------- 1 | title: "What do software architects at Khan Academy do?" 2 | published_on: May 14, 2018 3 | author: Kevin Dangoor 4 | team: Infrastructure 5 | ... 6 | 7 | “Architect” is a new role in Khan Academy’s engineering team this year, and my colleague, [John](https://twitter.com/jeresig), and I have stepped into this role. John has been with Khan for seven years now, mostly focused on frontend development. I’ve just reached three years here, having spent time in frontend development and engineering management. There are many possible paths to this role, and I’ve seen quite a few definitions of it, so I thought I’d share our view of being an architect. 8 | 9 | ## The role of an architect 10 | The Wikipedia article about software architects [leads with this](https://en.wikipedia.org/wiki/Software_architect): 11 | 12 | > A software architect is a software expert who makes high-level design choices and dictates technical standards, including software coding standards, tools, and platforms. 13 | 14 | My view is a subtle shift from that one: **an architect acts as a sort of product manager for the system in which software is built**. This “system” consists of the coding standards, tools, platforms, and even processes used by the engineers on the team to build features for their users. Architects look for ways in which the system can better serve the engineers. 15 | 16 | The Wikipedia definition describes the architect as “making” the design choices and “dictating” the standards. For me, that evokes images of an architect handing a scroll to a messenger who then walks among the engineers declaring, “On this, the 14th of May, 2018, we hereby decree that all files shall use four spaces for indentation.” 17 | 18 | As a product manager for the system, I look out for ways to make things better and then work with the engineers and engineering management to make the changes come to fruition. It’s a lot more collaboration than it is dictation. 19 | 20 | That’s enough vague, high-level talk. Let’s talk about things that actually consume time in my day. 21 | 22 | ## We guide architecture change 23 | Though I was joking about the Great Indentation Decree of May 2018, we _do_ need coding standards to help engineers make sense of our system. If you’ve ever been part of a protracted technical argument, you know that these can sometimes be draining and not a productive use of time. 24 | 25 | At Khan Academy, we’ve adopted [DACI](https://www.atlassian.com/team-playbook/plays/daci) as a decision-making framework. DACI stands for Driver, Approver(s), Contributors, Informed and specifies the roles involved in making a decision. The architects and other engineering leaders act as the “approvers” on architecture changes. You could interpret that as “the architects make the decision,” which makes this process sound like the “dictating change” approach from Wikipedia, but that’s not how DACI works. 26 | 27 | In DACI, the Driver makes sure that the change has the right contributors involved and the Approvers “own” the decision, ensuring that concerns have been accounted for. Are there potential security implications to the change? If so, we’ll make sure our security lead is a contributor. 28 | 29 | The Driver and Contributors together work out the details of the change and, ideally, the actual decision is self-evident by the end of their work. As the approvers in those cases, architects and other engineering leaders make sure that all of the questions have been resolved. 30 | 31 | In the cases in which there are two choices, we have to pick one, and the differences between the two are minor, the approver will indeed make the choice. This wasn’t necessary in the vast majority of cases we’ve seen so far. 32 | 33 | Every architecture change we make goes into a [decision log](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) so that future-us can understand _why_ our system works the way it does, making it easier to understand when it should change again. 34 | 35 | This process is intentionally designed to be lightweight, so that we can move quickly and efficiently. A simple, uncontroversial change can be written up and approved in a day. 36 | 37 | What kinds of changes have we made this way? A few recent examples: 38 | 39 | * We’ve adopted rules for how we would deprecate public API endpoints 40 | * We’ve created standards for React components that we use site-wide 41 | * We have a second language (Kotlin) that is an acceptable choice for building services 42 | 43 | Being a part of all of these changes helps architects maintain a broad view of work that’s happening in our system, which helps us guide further changes to it. 44 | 45 | ## Architects help our guilds 46 | Khan Academy’s teams are built around product areas, so we’ve established guilds that look out for our technology across the product areas. We have four guilds currently: web frontend, backend, mobile, and data engineering. 47 | 48 | Guilds have the ability to influence how a portion of our development time is used, for things like [breaking apart our Python code](http://engineering.khanacademy.org/posts/python-refactor-1.htm) or creating a [wholly new set of UI components](https://github.com/Khan/wonder-blocks). John and I help organize the guild work for the frontend and backend guilds, respectively. 49 | 50 | ## Communication 51 | Architects spend a lot of time on communication tasks to help everyone stay in sync about the current state of our system and how it’s evolving. Architecture diagrams, roadmaps, project plans, this blog, a newsletter of relevant technology stories, and meetings are all pieces of what we do as architects. 52 | 53 | The newsletter (see below) helps ensure that everyone in engineering has seen some stories about how technology that affects us is changing. 54 | 55 | ![Signal Boost newsletter](/images/architects/signalboost.png) 56 | 57 | The diagrams, which we’re creating in part following the [C4 model](http://c4model.com), are a useful tool for communicating about our system. I’ve been working on a proposal for a change to our system and I was able to take pieces out of our diagrams and create new ones that made the step-by-step progression in the proposal much clearer. 58 | 59 | ![Sample c4 architecture diagram](/images/architects/c4sample.png) 60 | 61 | ## Code 62 | Architects are programmers and John and I both exercise those skills as part of the job. 63 | 64 | ## Managers and architects 65 | Architects aren’t typically people managers, and that is the case for both John and me. When it comes to our development process and how projects are going, our managers take the lead. Technical leadership within engineering is shared by architects and managers, with managers taking more responsibility for delivery of the product (the “what”) and architects focusing more on the state of the architecture (the “how”). 66 | 67 | I spend time each week talking with an engineer on the team to find out how things are working for them. That helps me get a view across the organization of how the system is working, and a cross-cutting perspective I can share with managers when there are things we can improve. 68 | 69 | ## PMs for the system 70 | I’m fond of the “product manager for the system” definition of software architect. The users of the system are our software engineers, QA, and others who are ultimately working to deliver features for learners, teachers and coaches who use Khan Academy, and we want to help that system deliver better and faster. 71 | 72 | _Thanks to Marta Kosarchyn, David Flanagan, Scott Grant, and John Resig for their feedback on this post._ -------------------------------------------------------------------------------- /src/posts/original-serverless.md: -------------------------------------------------------------------------------- 1 | title: "The Original Serverless Architecture is Still Here" 2 | published_on: May 31, 2018 3 | author: Kevin Dangoor 4 | team: Infrastructure 5 | ... 6 | 7 | This month, my colleague Dave Rosile and I went to [GlueCon](http://gluecon.com) 2018 in sunny Denver, Colorado. The organizers did a fantastic job putting together a conference around modern server architectures. 8 | 9 | In talking about those architectures, there were a bunch of talks related to container orchestration (which is now synonymous with [Kubernetes](https://kubernetes.io)), and [serverless approaches](https://martinfowler.com/articles/serverless.html). 10 | 11 | ## Kubernetes gives you flexibility 12 | 13 | Kubernetes and the ecosystem around it strike me as something distinctly different from what we had in the decades before it. Starting with Docker containers as a common format for deploying complete and immutable images of software, Kubernetes layers on management of those containers to start and stop them as needed. Docker is still relatively low-level, describing only one container, so there’s [Helm](https://helm.sh) to help package up larger solutions. There are a few “ingress controllers” to manage traffic coming into a Kubernetes service, and a number of ways to monitor what’s going on within your Kubernetes clusters (for example, [Prometheus](https://prometheus.io) and [InfluxDB](https://www.influxdata.com/products/integrations/)). There’s even [Istio](https://istio.io) and other service mesh solutions to control how your services talk to one another. And none of this ties you to a single company’s cloud. 14 | 15 | That ecosystem is powerful and flexible, enabling you to put together whatever collection of software you think is right for the job at hand. Flexibility doesn’t come free, though. In some cases, you’ll have to choose among several options, and some of the tools are less mature than others. Once you’re done, you can end up with a finely tuned collection of services that efficiently deliver what your users want. But it takes work and you’ll likely stub your toes a couple of times getting there. 16 | 17 | All of the excitement and development around Kubernetes today means that future engineers will likely be able to build efficient, manageable services _without_ the toe stubbing. 18 | 19 | ## Serverless lets you focus on your application 20 | “Serverless” architectures have been built around services like [Firebase](https://firebase.google.com), which allow developers to build client applications that connect to a server infrastructure the developer doesn’t need to manage. More recently, serverless has come to encompass “functions as a service,” most notably [Amazon’s Lambda](https://aws.amazon.com/lambda/). With Lambda, you’re still writing server-side code, but just in functions that take in data and send out a result, with the “server” part of it completely abstracted away. These functions are stateless and connect to services like [DynamoDB](https://aws.amazon.com/dynamodb/) or [Aurora](https://aws.amazon.com/rds/aurora/) for persistence. 21 | 22 | A single function likely can’t act as a whole backend for an application. Building a serverless app therefore often includes creating multiple functions, setting up an [API Gateway](https://aws.amazon.com/api-gateway/), perhaps some queues and [Step Functions](https://aws.amazon.com/step-functions/) to glue things together, [X-Ray](https://aws.amazon.com/xray/) for debugging, etc. 23 | 24 | All of those services are fully managed and scalable and don’t require the same attention to resource usage that Kubernetes does. Serverless frameworks (including [Serverless](https://serverless.com), [Up](https://github.com/apex/up), and [.architect](https://arc.codes)) help provide the glue and make local development easier. 25 | 26 | Theoretically, if you build an application around functions, you can deploy a change in _seconds_, which is pretty cool. Functions-as-a-service (FaaS) takes the microservice idea as far as it can go (assuming no one’s about to introduce “line of code as a service”), making deployment units as small as can be, with the complexity coming in the form of coordination between those units. 27 | 28 | In addition to the simple scaling offered by serverless, you also win by only paying for what you use. You don’t need to have frontend and database server instances running when nothing is happening on your site. A Kubernetes-based system could conceivably win on cost by building services that are finely tuned for your use cases. But that tuning takes effort and, without that effort, you’re more likely to be reserving more instances than a serverless implementation would. 29 | 30 | Serverless frameworks today are young and largely tied to a single cloud provider, but over time I suspect we’ll see even easier ways to glue together autoscaling services from whichever providers give you the features you need. 31 | 32 | ## The serverless architecture used at Khan Academy 33 | Why do people want serverless architectures? In my opinion, the reason to opt for a serverless architecture is because you want to focus on your application code and not focus on the infrastructure around building reliable, scalable services. 34 | 35 | From the get-go, Khan Academy wanted a system that could scale as our usage increased, without requiring us to scale an operations team to match. Back when we started, the term “serverless” didn’t even exist and there were only a handful of services that would help you autoscale, notably Heroku and Google App Engine. We chose App Engine. 36 | 37 | Today, Google compares App Engine to the new serverless frameworks, because it bundles [local development](https://cloud.google.com/appengine/downloads), data persistence (through [Datastore](https://cloud.google.com/appengine/docs/standard/#datastore)), monitoring and debugging (through [Stackdriver](https://cloud.google.com/stackdriver/)), and other features in simple, deployable units. You can even do [microservices with App Engine](https://cloud.google.com/appengine/docs/standard/python/microservices-on-app-engine). 38 | 39 | App Engine has evolved over time, [adding Node to the supported languages on the Standard Environment](https://www.infoq.com/news/2018/05/gae-node), with the [Flexible Environment](https://cloud.google.com/appengine/docs/flexible/) going even further with support for custom Docker containers. 40 | 41 | A framework like [.architect](https://arc.codes) pulls together the setup of a whole collection of AWS services to provide a cohesive experience. App Engine, too, was built to give developers an all-inclusive experience, and it provides that experience whether you’re deploying a single 100-line file or a much larger application. 42 | 43 | I’m excited about all of the work going on around Kubernetes and function-based server architectures. The Kubernetes ecosystem is working out ways to provide tons of flexibility while improving manageability of services. The serverless crowd is working out new ways to spin up microservices where the hardware and networking is abstracted away. While these new technologies mature, our original serverless setup of App Engine continues to work well for us. 44 | 45 | That said, whether you’re deploying to App Engine or EC2 today, I think it’s important to keep an eye on the fast-moving orchestration space. We’re rapidly moving toward a world in which you can deploy applications constructed of just the services you need, with far greater cloud independence. 46 | 47 | _Thanks to Ben Kraft, Craig Silverstein, Amos Latteier, and Marta Kosarchyn for their feedback on drafts of this article._ 48 | 49 | Discuss [on Hacker News](https://news.ycombinator.com/item?id=17197085) 50 | -------------------------------------------------------------------------------- /src/posts/react-native-monorepo.md: -------------------------------------------------------------------------------- 1 | title: "Migrating to a Mobile Monorepo for React Native" 2 | published_on: May 29, 2017 3 | author: Jared Forsyth 4 | team: Mobile 5 | ... 6 | 7 | Over the past few months, we've been adding some [React 8 | Native](https://github.com/facebook/react-native) to our existing 9 | [iOS](https://itunes.apple.com/us/app/khan-academy-you-can-learn-anything/id469863705?mt=8) 10 | and 11 | [Android](https://play.google.com/store/apps/details?id=org.khanacademy.android&hl=en) 12 | apps. We started out by just creating a `react-native` repository and adding 13 | it as a submodule of our respective `ios` and `android` git repositories, but 14 | we quickly found that there was a fair amount of friction in coordinating 15 | between the three. We've now moved all of our mobile-related repositories 16 | (including a `mobile-scripts` and a `shared-webview` repository) into a single 17 | `mobile` [monorepo](https://danluu.com/monorepo/). 18 | 19 | ## Why did we do it? 20 | 21 | When making changes to the bridge that JavaScript uses to get data from the 22 | native side, we need to make the change to both the Android and the iOS 23 | codebases, or else we'll get runtime errors. With three repositories to work 24 | with, it was too easy to forget to add one or the other (as you're generally 25 | developing with just one simulator open), resulting in a broken experience for 26 | one platform or the other. 27 | 28 | We'd also get into a state where the `master` branch of `react-native` 29 | contained some changes that had only been coordinated with e.g. the `ios` 30 | repo, and the `android` repo's `react-native` submodule would be several 31 | commits behind. Then someone working on the Android side would update the 32 | submodule, and they'd have to track down all the breakages. 33 | 34 | In short, we started running into a lot of synchronization issues that 35 | wouldn't happen if all of the code was in the same repository. With a 36 | monorepo, pull requests could be combined, reviews would be more coherent, and 37 | it would be easier to verify correctness between codebases. 38 | 39 | ### Anticipated pros 40 | 41 | - changing bridge between JavaScript and native would be easier because 42 | you'd only need a single pull request instead of three 43 | - as a result, they would be less likely to get out of sync 44 | - not having to mess with submodules 🎉 45 | 46 | ### Anticipated cons 47 | 48 | - we might lose Git history (this didn't turn out to be the case) 49 | - we'd have to change all of our Jenkins build scripts 50 | - moving all our developers to a new repository requires coordination 51 | - we'd lose any in-flight branches and open PRs to the old repositories (we 52 | actually found a solution for this too) 53 | 54 | ## What were the steps? 55 | 56 | ### Setup 57 | 58 | Make a fresh monorepo: 59 | 60 | ```sh 61 | mkdir mobile; cd mobile; git init . 62 | ``` 63 | 64 | Have all the repos that you want to combine cloned and fully up to date 65 | 66 | ```sh 67 | $ ls . 68 | mobile 69 | android 70 | ios 71 | react-native 72 | ``` 73 | 74 | ### Preparing the repos for merging 75 | 76 | Clone each repo into `m_reponame` (using `android` as the example) and then 77 | move all files into a subfolder (except for `.git`, of course). 78 | 79 | ```sh 80 | git clone android m_android 81 | cd m_android 82 | mkdir android 83 | mv * .* android 84 | mv android/.git . 85 | ``` 86 | 87 | Then commit the result: `git add . && git commit -m'move to subfolder'` 88 | 89 | With the code for each respective codebase moved into a subdirectory, we're 90 | then able to move them all into a single repository without having them clash 91 | with each other. To illustrate, here's what the rough directory structure 92 | looks like: 93 | 94 | ``` 95 | android/ 96 | build.gradle, etc. 97 | ios/ 98 | AppDelegate.m, etc. 99 | react-native/ 100 | index.ios.js, etc. 101 | m_android/ 102 | android/ 103 | build.gradle, etc. 104 | m_ios/ 105 | ios/ 106 | AppDelegate.m, etc. 107 | m_react-native/ 108 | react-native/ 109 | index.ios.js, etc. 110 | ``` 111 | 112 | The monorepo will then have the following structure: 113 | 114 | ``` 115 | mobile/ 116 | android/ 117 | build.gradle, etc. 118 | ios/ 119 | AppDelegate.m, etc. 120 | react-native/ 121 | index.ios.js, etc. 122 | ``` 123 | 124 | ### Merge each `m_reponame` into the monorepo 125 | 126 | Turns out git has super powers, and can totally merge in multiple unrelated 127 | repositories and preserve all the relevant git history. Who knew? 128 | 129 | ```sh 130 | cd mobile 131 | git fetch ../m_android 132 | git merge FETCH_HEAD --no-ff --allow-unrelated-histories \ 133 | -m 'merging in android repo' 134 | ``` 135 | 136 | Again, do this for each repository that you need to merge in. 137 | 138 | One thing I'm glossing over (that you'll have to figure out manually) is 139 | various dotfiles that you want to be shared. `.gitignore` is fine being in the 140 | respective subdirectories, but in our case we use Phabricator, and so we 141 | needed to make a top-level `.arcconfig` file that merged the `.arcconfig`s 142 | from the previous three repositories. 143 | 144 | I also had to manually bring over submodules, by re-cloning them in the new 145 | monorepo and checking them out at the commit where they were pinned in the 146 | pre-monorepo repositories. 147 | 148 | And of course the `react-native` folder was in a different place now that it 149 | wasn't a submodule, but a peer, to the iOS and Android codebases, so we had to 150 | update various relative paths and build scripts. 151 | 152 | ### Bringing in new changes from the old repos 153 | 154 | After creating the monorepo, our team had a flex week where they could still 155 | operate on the old repositories, so that they could land any outstanding code 156 | changes that they had inflight and move over at their own pace. 157 | 158 | If you do the same, here's how to bring in new changes to the old 159 | repositories: 160 | 161 | ```sh 162 | (cd android && git pull) 163 | cd m_android 164 | git pull ../android --no-ff \ 165 | -m 'merging in latest android changes' 166 | ``` 167 | 168 | At this point **check for new files**. Files that were added in the `android` 169 | repo after the monorepo move will not get automatically moved into the 170 | `m_android/android` subdirectory. It will be pretty obvious, because the only 171 | directory in the `m_android` repo should be `android`. If there's anything 172 | else there, `git mv the_new_thing android && git commit -am "moving new files 173 | into subdirectory"` before continuing. 174 | 175 | ```sh 176 | cd ../mobile 177 | git fetch ../m_android 178 | git merge FETCH_HEAD --no-ff \ 179 | -m 'merging in latest android changes' 180 | ``` 181 | 182 | This will even work if there have been changes committed to the monorepo, 183 | although you may have to resolve merge conflicts. 184 | 185 | ### Following Git history across the monorepo divide 186 | 187 | One of the things that surprised me the most was that Git was totally able to 188 | track the changes, even though they came from three different repositories. 189 | When doing `git log` for a specific file, you just need to add the `--follow` 190 | option (which tells Git to follow the file across renames), and everything 191 | works! 192 | 193 | ## And that's it! 194 | 195 | We've been working with the monorepo for over a month now, and it's been well 196 | worth it. If you're integrating React Native into two existing apps that are 197 | currently in separate repositories, you might benefit from the shift as well! 198 | -------------------------------------------------------------------------------- /src/posts/kanbanning-learnstorm-dev-process.md: -------------------------------------------------------------------------------- 1 | title: "Kanbanning the LearnStorm Dev Process" 2 | published_on: December 7, 2015 3 | author: Kevin Dangoor 4 | team: Web Frontend 5 | ... 6 | 7 | In his [New Employee's Primer](http://engineering.khanacademy.org/posts/new-employees-primer.htm) 8 | post from July, Riley Shaw wrote a little about our **T**eams **I**nitiatives 9 | **P**rojects (TIP) style of thinking about our development work. Here's a quick 10 | summary of TIP: 11 | 12 | * Teams represent areas of deep domain expertise like mobile, design, 13 | data science, 14 | * Initiatives are cross-functional, multi-project efforts that the 15 | organization believes are worth a significant investment, and 16 | * Projects are specific pieces of work that are scoped to take 2-5 weeks for 17 | 1-3 people. 18 | 19 | A five-week, three-person project is substantial, so that gives you 20 | an idea of where an initiative starts in terms of scope. 21 | 22 | TIP was adopted in February, so 2015 is the first year in which we've had 23 | initiatives. We don't have a specific process that each initiative uses 24 | to manage itself, which gives each initiative the freedom to figure out its 25 | best path forward. 26 | 27 | ## LearnStorm 28 | 29 | I'm working on the [LearnStorm](http://learnstorm2016.org) initiative. 30 | LearnStorm is a math challenge for students in 3rd-12th grades (and the 31 | Irish equivalents) in the Bay Area, Chicago, Idaho and Ireland. This 32 | initiative includes a collection of features that we're building into Khan 33 | Academy and a large amount of work from our programs team to organize things 34 | like helping teachers register students and in-person events. There are a bunch 35 | of us working over several months to make this challenge the best it can be. 36 | 37 | ## Our initial development process 38 | 39 | When we started development at the beginning of September, we used 40 | an approach similar to what other initiatives were doing at the time: 41 | we imagined the initiative as a collection of projects. We had rough ideas 42 | about how long each project would take and put the projects on a first draft 43 | timeline in a Google Sheets spreadsheet. We also had a Trello board that we 44 | used to show the projects in progress and the projects that were coming up. 45 | This was a useful way to think about the project at this early stage, but 46 | it didn't take long for us to start feeling limitations in how we could adapt 47 | our plans to handle new information. 48 | 49 | Around the beginning of November, we switched from a general "working on 50 | projects" mode to a "need to launch signups" mode. It became important to 51 | track many small and medium sized tasks that needed to be done 52 | before we could open up signups. Over a crazy few days we moved from Trello 53 | checklists to a Google Doc and finally to a list in Asana. 54 | 55 | That was a bumpy time. 56 | 57 | ## Smoothing the process for the second phase 58 | 59 | LearnStorm is different from other Khan Academy initiatives to date in that we 60 | have some hard deadlines. The challenge *will* start January 29th, 2016 for 61 | example. When it comes to getting a project out, there's the idea that you've 62 | got three levers to play with: scope, time and resources. For LearnStorm, the time 63 | is fixed. We also have the whole team in place that we're going to have, so 64 | the resources lever is also fixed (plus: [Mythical Man-Month](https://en.wikipedia.org/wiki/The_Mythical_Man-Month)). 65 | Scope is the *only* thing we have to play with in making sure that the 66 | challenge is ready on time. 67 | 68 | Two to five week-long projects can contain quite a bit of scope and don't 69 | lay that scope out in a way that makes it easy to choose between alternatives. 70 | Larger projects also make it harder to change priorities as we learn new things 71 | about the project. 72 | 73 | What if there was a process that allowed us to easily see how the project is 74 | progressing and reprioritize features based on new information? 75 | 76 | ## Enter Kanban 77 | 78 | I had [experience](http://www.blueskyonmars.com/2014/02/18/bracketsscrumtokanban/) 79 | with [Kanban](https://en.wikipedia.org/wiki/Kanban_%28development%29) in the 80 | past and thought it seemed like a good fit for our needs. Here is Wikipedia's 81 | description of Kanban for software development: 82 | 83 | > Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see. Team members pull work from a queue. 84 | 85 | Just-in-time delivery of the most important work is what makes Kanban work well for us. 86 | Each work item that we put into our queue in Trello is no more than a few days 87 | long. If needed, the priorities for the whole team can be changed in just a few 88 | days without interrupting the work that's already in progress. 89 | 90 | Plus, these small work items also provide a lot of choices for prioritization. 91 | When working through a couple of big features, there might be pieces of each 92 | that are lower priority and might be skipped entirely. 93 | 94 | ## But where's the big picture? 95 | 96 | Since the beginning of the initiative, we've been having regular retrospectives 97 | to improve how we work. We recently had our first retro since moving to Kanban 98 | and there was a lot of positive feedback. Developers on the team found the 99 | process of grabbing the next work item and running with it to be a good way to 100 | focus on the most important work. The flip side is that it's hard to see the 101 | big picture progress we're making on the overall project. 102 | 103 | We think a partial fix for this is easy: just make a document or diagram that 104 | groups the cards on the Kanban board with the overall project goals that 105 | they're bringing to life. I say that this is a partial fix, because there is 106 | a tradeoff in adopting small units of work as we have. It's much easier to 107 | see how 5 bigger projects become a whole than 25 smaller work items. And some 108 | of those small work items may *never* even be done. 109 | 110 | ## Choice of tools 111 | 112 | We write automated tests and try things out along the way, but we're 113 | still planning to have a focused testing and bug fixing period at the end of 114 | the project. 115 | 116 | When we were getting ready to launch signups, Asana's straightforward list 117 | view worked well for rapidly collecting and sorting all of the feedback we 118 | got from the testers. I've found it much harder to work with Trello boards 119 | that have a large number of items on them, so we'll probably switch back 120 | to Asana for that phase at the end of the Initiative. 121 | 122 | Trello and Asana store similar kinds of data, but the views and UI features 123 | make a huge difference in how you approach and use the tools. Unsurprisingly, 124 | people have used their APIs to present different views on top of the same 125 | data, but we haven't explored those third party tools yet. 126 | 127 | ## Pick a process and improve 128 | 129 | Kanban is not the "one process to rule them all". In fact, Kanban isn't 130 | a single process at all, but rather a way to think about and evolve 131 | the process you have. [Continuous improvement](https://visualstudiomagazine.com/articles/2013/06/01/continuous-improvement-and-the-agile-restrospective.aspx) 132 | is the name of the game, and that's what we've been going for. 133 | Our use of Kanban has helped us to collect, visualize and work on the most 134 | important things as our initiative evolves. 135 | 136 | -------------------------------------------------------------------------------- /src/posts/using-static-analysis-in-Python-and-JavaScript-to-make-your-system-safer.md: -------------------------------------------------------------------------------- 1 | title: "Using static analysis in Python, JavaScript and more to make your system safer" 2 | published_on: July 26, 2018 3 | author: Kevin Dangoor 4 | team: Infrastructure 5 | ... 6 | 7 | "Linting" source code to look for errors is nothing new ([the original “lint” tool turned turned forty this year!](https://en.wikipedia.org/wiki/Lint_(software))), but most places I worked prior to Khan Academy didn’t use linters as extensively as we do here. So, I thought I’d share a bit about a few of our custom linters in hopes that others may invest a little time to prevent more bugs. 8 | 9 | ## For more than just formatting 10 | JavaScript programmers will be familiar with tools like [ESLint](https://eslint.org) and Python programmers may be familiar with [pylint](https://www.pylint.org). Teams spend time arguing about what their code style is and then configure the linters to enforce that consistent style. For many folks, I’d imagine that their primary experience with linters is something along the lines of “oh, that’s the tool that complains when I put my brace in the wrong place.” 11 | 12 | I’m a big fan of [Prettier](https://prettier.io), which has completely eliminated both formatting errors _and_ discussion of code formatting for our JavaScript code. Even with code formatting being a “solved problem”, we rely more on linters than ever. They serve the important purpose of maintaining code quality by preventing known bad patterns from sneaking in. 13 | 14 | ## Staying in sync 15 | In our web application, we’ve got code written in JavaScript, Python, and Kotlin. One bit of complexity that naturally comes up when from having multiple languages is that sometimes you’ll need to keep files in sync. Imagine that we’ve duplicated a small bit of logic between two of the languages, or that there’s an interface shared between the two that must be changed in tandem. Sure, we do our best to minimize those case, but they can be hard to avoid entirely. 16 | 17 | Our `code_syncing_lint.py` linter is set up to handle this problem. It defines this comment format: 18 | 19 | ```py 20 | # sync-start: 21 | # sync-end: 22 | ``` 23 | 24 | `tag` is a name you give to the block of code. The linter ensures that if you make a change in one file in a given commit, there must also be a change in the block with the same `tag` name in `filename` as well. 25 | 26 | ## Frontend best practices 27 | Khan Academy has been around for several years now and JavaScript development has changed a lot over those years. Where we used to use Handlebars and LESS files for defining our client-side views, we now use React with [Aphrodite](https://github.com/Khan/aphrodite/). At this point, if someone creates a new Handlebars or LESS file in our repository, they’re working against the direction we’re pushing for our frontend. 28 | 29 | So, we have a linter that makes sure that no new files of those types are created. That linter comes complete with a whitelist of the files we haven’t yet managed to get rid of. As much as possible, when we introduce a new lint rule, we fix up all of the lint discovered by the rule. Rewriting a Handlebars template as React components is a non-trivial change, so this linter has a whitelist of those pre-existing files that are allowed to break the rule. 30 | 31 | ## You can lint images, too 32 | One of our engineers, Colin Fuller, noticed that some of the images on our [team page](https://www.khanacademy.org/about/the-team) looked off. The page is a grid of photos cropped to the same sizes, but some of the photos looked stretched. During our February [Healthy Hackathon](http://engineering.khanacademy.org/posts/healthy-hackathons.htm), Colin wrote a linter that double checks that every team photo is the same size. The linter is only about 50 lines of Python, comments included. 33 | 34 | ## Avoiding tests that accidentally overwrite others 35 | Have you ever copy/pasted an existing unit test to create a new test case for a different variation? Sometimes, all you want to do is call the function under test with different arguments, so copy/paste is the easy solution. 36 | 37 | Imagine you have a piece of a test class that looks like this, after a copy/paste: 38 | 39 | ```py 40 | def test_foo1(self): 41 | self.assertEqual(1, foo("one")) 42 | 43 | def test_foo1(self): 44 | self.assertEqual(2, foo("two")) 45 | ``` 46 | 47 | It’s pretty easy to forget to change the test method name and not notice that the original test will no longer be called. We have a linter that watches for this. 48 | 49 | ## Avoiding problems with third-party libraries 50 | Third-party libraries don’t always work the way we’d want them to. Many times, the right answer is to put up a pull request for the library. But what happens if the behavior of the library is _by design_? 51 | 52 | We’ve got a case of that in our codebase. For example, we use [persistgraphql](https://github.com/apollographql/persistgraphql) to extract GraphQL queries from our client-side code so that we can allow only specific queries to run. The problem we ran into is that persistgraphql reformats the queries in a way that works fine for their main use case, but could make the queries not match up with what our server-side code expects. Our solution is a linter that guarantees that the query in the JavaScript code will exactly match the query expected by the server. 53 | 54 | ## You can import _this_ but you can’t import _that_ 55 | As part of our [Great Python Refactoring](https://engineering.khanacademy.org/posts/python-refactor-1.htm), we instituted new rules about which code was allowed to import which other code, to help avoid future similar tangles. We could impose restrictions via Python import hooks, but we wouldn’t find out about those problems until runtime. Our `components_lint.py` linter ensures at commit time that we aren’t breaking the rules. 56 | 57 | Of course, the Great Python Refactoring didn’t take care of _every_ bothersome import, but the linter has a simple whitelist that we’ll whittle down as we continue to clean things up. 58 | 59 | ## Tip: Keep linters fast with regular expressions 60 | Yes, yes, we all know [you can’t parse HTML with a regular expression](https://stackoverflow.com/a/1732454/15851). When you need to be correct 100% of the time, you need to use a proper parser. But many times a regular expression will suffice and be _much_ faster. While regular expression syntax can seem quite obscure (or become [a maintenance nightmare](https://stackoverflow.com/a/800847/15851)), regexes can actually be easier to understand than code designed to traverse a parse tree. 61 | 62 | As an added bonus, linters like our `code_syncing_lint` mentioned earlier can work on Python, JavaScript, and Kotlin files without needing three separate parsers and a whole lot more code. 63 | 64 | But beware! It’s easy for a regex to not properly handle legitimate, real world code files, so just keep that tradeoff in mind. Linters are like any other code, though, so you can write unit tests to verify the expected cases as we have for ours. 65 | 66 | ## Lint all the things! 67 | Once you have the basic hooks in place to run linters automatically, you’ll doubtless find many ways in which you can prevent common sorts of bugs from creeping in to your system. [ESLint has rules](https://eslint.org/docs/rules/) to help you with frequent JavaScript mistakes, but I’d bet there are other potential pitfalls that are unique to your environment. 68 | 69 | I hope this tour of some of our linters gives you ideas for some of your own. 70 | 71 | _Thanks to Ben Kraft, Craig Silverstein, and Amos Latteier for their suggestions of good example linters from our code, and Scott Grant for editing advice._ -------------------------------------------------------------------------------- /src/posts/receiving-feedback.md: -------------------------------------------------------------------------------- 1 | title: "Receiving feedback as an intern at Khan Academy" 2 | published_on: October 26, 2015 3 | author: David Wang 4 | team: Infrastructure 5 | ... 6 | 7 | 8 | ![](/images/receiving-feedback/feedback.jpg) 9 | 10 | When my first piece of code was reviewed at Khan Academy, my mentor [Dylan](http://www.dylanv.org/) prefaced his comments with the following: 11 | 12 | ![](/images/receiving-feedback/phab.jpg) 13 | (Kamens’s post [here](http://bjk5.com/post/3994859683/code-reviews-as-relationship-builders-a-few-tips), Tom’s post [here](http://www.arguingwithalgorithms.com/posts/13-03-14-code-reviews)) 14 | 15 | And to be honest, I chuckled when I read that: I had already expected seeing a bunch of red during my first code review, and if I received a [ShipIt](/images/receiving-feedback/shipit.jpg) on my first go without any comments whatsoever, I would’ve questioned the efficacy of the code review process at the company :). I asked Dylan why he felt the need to open his code review with this lead-in, and he described his experience with his first code review as an intern. It would have been discouraging to receive so much critical feedback all at once, had he not received the same piece of advice. Somewhere in the middle of that conversation, he let me know that the quality of my code had little to do with the quality of my person, and that code critiques != character critiques. 16 | 17 | This interaction with Dylan led me to wonder about feedback - as a whole in addition to code feedback - more than once throughout the course of the summer. What is Khan Academy’s view on giving feedback in general, and giving feedback to interns in particular? Why does it hurt so much to receive criticism, even when it is delivered with the best of intentions? Why is it that we feel we have to tread so carefully when we’re delivering it? 18 | 19 | Khan Academy and feedback 20 | --- 21 | 22 | While every one of these questions can have an entire blog post devoted to each of them, I’m going to just give a brief overview of what I’ve discovered on all of them here. First, regarding KA’s view on feedback, after chatting with managers and poring through docs, I discovered a page on the company wiki about developing a feedback culture. Perfect! It said that the company views feedback as a gift: 23 | it’s given with the best of intentions 24 | it should be considered a positive sign that my colleagues are willing and choose to invest in me (and my development). 25 | All cheesiness aside, I think I’ve come to really appreciate that metaphor as much as that other one about [today being a gift](http://www.brainyquote.com/quotes/quotes/b/bilkeane121860.html). I can pinpoint instances, both in work and in school, where I’ve gotten the impression that people have given up on me. And to be honest, being ignored for me is a much worse feeling than having criticism barraged at me. In addition to this metaphor, KA views a feedback culture as one that is regular / ongoing and 360 degrees, where anyone feels comfortable asking for feedback from anyone else. 26 | 27 | And for interns? As part of the [mentor/menteeship experience](http://bjk5.com/post/23266999170/how-intern-mentorship-works-at-khan-academy) at KA, I had weekly 1-on-1s, where I checked in with my mentor and talked about how the last 7 days have been, and whether I was getting what I wanted out of my summer experience. While it’s acknowledged that these 1-on-1s are a good time to exchange feedback between mentor and mentee, it’s not always explicitly stated that that is one of the goals. Thus, something I’m very glad I did was at the end of each 1-on-1, asking for one actionable piece of information that I could change or focus on for the next week. 28 | 29 | In addition to the weekly 1-on-1s, one thing I’m glad Khan Academy does is offer a more formal midpoint evaluation for each of its interns. And from the context of a single internship, the midpoint evaluation is (in my opinion) more valuable than the final evaluation: it’s less a performance evaluation (where we discuss how us interns have performed compared to what has been expected of us) and more a coaching opportunity. By getting recommendations on where we can improve, we can take steps towards playing at a higher level. And if there’s 6 weeks left to an internship during its midpoint compared with 0 at the end of the internship, the feedback given at the halfway mark allows for greater growth. 30 | 31 | The process as a whole 32 | --- 33 | 34 | In general, I've often asked myself why receiving feedback is so hard. After introspecting, talking to some folks, and [digging](http://fsap.cornell.edu/cms/fsap/resources/upload/How-to-Receive-Critical-Feedback.pdf) [around](https://open.buffer.com/how-to-give-receive-feedback-work/), I think that there are three things at stake: 35 | 36 | 1. We feel wronged if a piece of feedback seems inaccurate. This leads to a host of possible reactions: we deem the feedback as simply wrong, or maybe we believe that the person giving the feedback has made unfair assumptions about the motivations that drove our behavior. 37 | 2. We might reject the feedback because of how we feel about the giver. Perhaps it was unsolicited, perhaps we believe that person doesn’t have credibility in what they’re critiquing us on, perhaps the feedback hasn’t been communicated well, or perhaps we’re unsure of the intentions of the feedback giver himself. 38 | 3. We perceive feedback as a threat: it might cause us to question our relationship with ourselves. One of our core human needs is to be accepted the way we are, and this is so ingrained in us that signs of rejection (through critical feedback, for instance) lead to the same flight-or-flight response in our limbic systems as actual threats to our physical safety. 39 | 40 | As to treading carefully when giving feedback, I think it's a matter of taking the cautious route after briefly taking the perspective of the feedback receiver. We understand how difficult it is to be on the opposite end, and we lean towards a soft approach to make the experience more positive and less stressful. 41 | 42 | Lastly, I believe that after all this discussion on feedback and constructive criticism, we should not underestimate the value of compliments. Not because they’re used as part of the ["feedback sandwich" technique](http://www.aafp.org/fpm/2002/1100/p43.pdf) to make people more open to feedback, but because it’s important for us to know when we’re doing things correctly and what we could be doing more of. To borrow [the wise Ben Kamens’s words](http://bjk5.com/post/3994859683/code-reviews-as-relationship-builders-a-few-tips), “If your team isn’t taking advantage of the chance to also acknowledge good work and the little sparks of genius that pop up here and there, you’re doing it wrong.” 43 | 44 | Takeaways 45 | --- 46 | Looking back at my time at KA, I’m glad that they’ve been willing to throw me in the deep end and hold me accountable for the work I’ve done. During these 12 weeks, I broke part of the site for the greater part of a day, gave a company-wide presentation on social psychology of all things, and helped build [a recommendation system](http://data.khanacademy.org/2015/09/so-you-want-to-build-recommender-system_8.html) for the company hackathon. And in the spirit of growth, I’m glad that my mentors Dylan and Sergei have encouraged me to try things that are outside of my comfort range, and have provided guidance, support, and feedback across all things technical and non-technical. 47 | 48 | When people ask me what I learned from my internship or what my most major takeaway was, I tell them the meta lesson I’ve reinforced in myself about learning, which can be described by any of the following synonymous statements: 49 | 50 | - To quote the eloquent [David Hu](http://david-hu.com/2015/01/14/khantemplations.html), “Assume you’re stupid so you can always be learning.” 51 | - From Zen Buddhist Shunryu Suzuki on adopting a beginner’s mindset, “In the beginner's mind there are many possibilities, but in the expert's mind there are few.” 52 | - With my own words: frame feedback as opportunities for coaching instead of evaluation, and frame mistakes as learning experiences instead of failures. 53 | 54 | In case you were wondering, Dylan’s comments to me in my first code review were to write a more descriptive commit message and test plan :). I hope I received the feedback well. 55 | 56 | --------------------------------------------------------------------------------