A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 69KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Making 1 million requests with python-aiohttp (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="http://pawelmhm.github.io/asyncio/python/aiohttp/2016/04/22/asyncio-aiohttp.html">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. Making 1 million requests with python-aiohttp (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="http://pawelmhm.github.io/asyncio/python/aiohttp/2016/04/22/asyncio-aiohttp.html">Source originale du contenu</a></h3>
  445. <p>In this post I’d like to test limits of <a href="http://aiohttp.readthedocs.org/en/stable/">python aiohttp</a> and check its performance in
  446. terms of requests per minute. Everyone knows that asynchronous code performs
  447. better when applied to network operations, but it’s still interesting to check this
  448. assumption and understand how exactly it is better and why it’s is better. I’m going
  449. to check it by trying to make 1 million requests with aiohttp client. How many requests per minute will aiohttp make?
  450. What kind of exceptions and crashes can you expect when you try to make such volume
  451. of requests with very primitive scripts? What are main gotchas that you need
  452. to think about when trying to make such volume of requests?</p>
  453. <h2 id="hello-asyncioaiohttp">Hello asyncio/aiohttp</h2>
  454. <p>Async programming is not easy. It’s not easy because using callbacks and thinking in terms of events
  455. and event handlers requires more effort than usual synchronous programming. But
  456. it is also difficult because asyncio is still relatively new and there are few
  457. blog posts, tutorials about it. <a href="https://docs.python.org/3/library/asyncio.html">Official docs</a>
  458. are very terse and contain only basic examples. There are some Stack Overflow questions
  459. but not <a href="http://stackoverflow.com/questions/tagged/python-asyncio?sort=votes&amp;pageSize=50">that many</a>
  460. only 410 as of time of writing (compare with <a href="http://stackoverflow.com/questions/tagged/twisted">2 585 questions tagged “twisted”</a>)
  461. There are couple of nice blog posts and articles about asyncio
  462. over there such as <a href="http://aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html">this</a>,
  463. <a href="http://www.snarky.ca/how-the-heck-does-async-await-work-in-python-3-5">that</a>, <a href="http://sahandsaba.com/understanding-asyncio-node-js-python-3-4.html">that</a> or perhaps even <a href="https://community.nitrous.io/tutorials/asynchronous-programming-with-python-3">this</a>
  464. or <a href="https://compiletoi.net/fast-scraping-in-python-with-asyncio/">this</a>.</p>
  465. <p>To make it easier let’s start with the basics - simple HTTP hello world -
  466. just making GET and fetching one single HTTP response.</p>
  467. <p>In synchronous world you just do:</p>
  468. <pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="nn">requests</span>
  469. <span class="k">def</span> <span class="nf">hello</span><span class="p">()</span>
  470. <span class="k">return</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">"http://httpbin.org/get"</span><span class="p">)</span>
  471. <span class="k">print</span><span class="p">(</span><span class="n">hello</span><span class="p">())</span></code></pre>
  472. <p>How does that look in aiohttp?</p>
  473. <pre><code class="language-python" data-lang="python"><span class="c">#!/usr/local/bin/python3.5</span>
  474. <span class="kn">import</span> <span class="nn">asyncio</span>
  475. <span class="kn">from</span> <span class="nn">aiohttp</span> <span class="kn">import</span> <span class="n">ClientSession</span>
  476. <span class="n">async</span> <span class="k">def</span> <span class="nf">hello</span><span class="p">():</span>
  477. <span class="n">async</span> <span class="k">with</span> <span class="n">ClientSession</span><span class="p">()</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
  478. <span class="n">async</span> <span class="k">with</span> <span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">"http://httpbin.org/headers"</span><span class="p">)</span> <span class="k">as</span> <span class="n">response</span><span class="p">:</span>
  479. <span class="n">response</span> <span class="o">=</span> <span class="n">await</span> <span class="n">response</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
  480. <span class="k">print</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
  481. <span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_event_loop</span><span class="p">()</span>
  482. <span class="n">loop</span><span class="o">.</span><span class="n">run_until_complete</span><span class="p">(</span><span class="n">hello</span><span class="p">())</span></code></pre>
  483. <p>hmm looks like I had to write lots of code for such a basic task… There is “async def” and “async with” and two “awaits” here. It
  484. seems really confusing at first sight, let’s try to explain it then.</p>
  485. <p>You make your function asynchronous by using <a href="https://www.python.org/dev/peps/pep-0492/#await-expression">async keyword</a> before function definition and using await
  486. keyword. There are actually two asynchronous operations that our hello() function performs. First
  487. it fetches response asynchronously, then it reads response body in asynchronous manner.</p>
  488. <p>Aiohttp recommends to use ClientSession as primary interface to make requests. ClientSession
  489. allows you to store cookies between requests and keeps objects that are common for
  490. all requests (event loop, connection and other things). Session needs to be closed after using it,
  491. and closing session is another asynchronous operation, this is why you need <a href="https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with"><code class="highlighter-rouge">async with</code></a>
  492. every time you deal with sessions.</p>
  493. <p>After you open client session you can use it to make requests. This is where another asynchronous
  494. operation starts, downloading request. Just as in case of client sessions responses must be closed
  495. explicitly, and context manager’s <code class="highlighter-rouge">with</code> statement ensures it will be closed properly in all
  496. circumstances.</p>
  497. <p>To start your program you need to run it in event loop, so you need to create instance of asyncio
  498. loop and put task into this loop.</p>
  499. <p>It all does sound bit difficult but it’s not that complex and looks logical if you spend
  500. some time trying to understand it.</p>
  501. <h2 id="fetch-multiple-urls">Fetch multiple urls</h2>
  502. <p>Now let’s try to do something more interesting, fetching multiple urls one after another.
  503. With synchronous code you would do just:</p>
  504. <pre><code class="language-python" data-lang="python"><span class="k">for</span> <span class="n">url</span> <span class="ow">in</span> <span class="n">urls</span><span class="p">:</span>
  505. <span class="k">print</span><span class="p">(</span><span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span><span class="o">.</span><span class="n">text</span><span class="p">)</span></code></pre>
  506. <p>This is really quick and easy, async will not be that easy, so you should always consider if something more complex
  507. is actually necessary for your needs. If your app works nice with synchronous code maybe there
  508. is no need to bother with async code? If you do need to bother with async code here’s how you do
  509. that. Our <code class="highlighter-rouge">hello()</code> async function stays the same but we need to wrap it in asyncio <a href="https://docs.python.org/3/library/asyncio-task.html#future"><code class="highlighter-rouge">Future</code></a> object
  510. and pass whole lists of Future objects as tasks to be executed in the loop.</p>
  511. <pre><code class="language-python" data-lang="python"><span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_event_loop</span><span class="p">()</span>
  512. <span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>
  513. <span class="c"># I'm using test server localhost, but you can use any url</span>
  514. <span class="n">url</span> <span class="o">=</span> <span class="s">"http://localhost:8080/{}"</span>
  515. <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>
  516. <span class="n">task</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">hello</span><span class="p">(</span><span class="n">url</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">i</span><span class="p">)))</span>
  517. <span class="n">tasks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
  518. <span class="n">loop</span><span class="o">.</span><span class="n">run_until_complete</span><span class="p">(</span><span class="n">asyncio</span><span class="o">.</span><span class="n">wait</span><span class="p">(</span><span class="n">tasks</span><span class="p">))</span></code></pre>
  519. <p>Now let’s say we want to collect all responses in one list and do some
  520. postprocessing on them. At the moment we’re not keeping response body
  521. anywhere, we just print it, let’s return this response, keep it in list, and
  522. print all responses at the end.</p>
  523. <p>To collect bunch of responses you probably need to write something along the lines of:</p>
  524. <pre><code class="language-python" data-lang="python"><span class="c">#!/usr/local/bin/python3.5</span>
  525. <span class="kn">import</span> <span class="nn">asyncio</span>
  526. <span class="kn">from</span> <span class="nn">aiohttp</span> <span class="kn">import</span> <span class="n">ClientSession</span>
  527. <span class="n">async</span> <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">url</span><span class="p">):</span>
  528. <span class="n">async</span> <span class="k">with</span> <span class="n">ClientSession</span><span class="p">()</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
  529. <span class="n">async</span> <span class="k">with</span> <span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">as</span> <span class="n">response</span><span class="p">:</span>
  530. <span class="k">return</span> <span class="n">await</span> <span class="n">response</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
  531. <span class="n">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">loop</span><span class="p">,</span> <span class="n">r</span><span class="p">):</span>
  532. <span class="n">url</span> <span class="o">=</span> <span class="s">"http://localhost:8080/{}"</span>
  533. <span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>
  534. <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">r</span><span class="p">):</span>
  535. <span class="n">task</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">fetch</span><span class="p">(</span><span class="n">url</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">i</span><span class="p">)))</span>
  536. <span class="n">tasks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
  537. <span class="n">responses</span> <span class="o">=</span> <span class="n">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">)</span>
  538. <span class="c"># you now have all response bodies in this variable</span>
  539. <span class="k">print</span><span class="p">(</span><span class="n">responses</span><span class="p">)</span>
  540. <span class="k">def</span> <span class="nf">print_responses</span><span class="p">(</span><span class="n">result</span><span class="p">):</span>
  541. <span class="k">print</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
  542. <span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_event_loop</span><span class="p">()</span>
  543. <span class="n">future</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">run</span><span class="p">(</span><span class="n">loop</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
  544. <span class="n">loop</span><span class="o">.</span><span class="n">run_until_complete</span><span class="p">(</span><span class="n">future</span><span class="p">)</span></code></pre>
  545. <p>Notice usage of <a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"><code class="highlighter-rouge">asyncio.gather()</code></a>, this collects bunch of Future objects in one place
  546. and waits for all of them to finish.</p>
  547. <h3 id="common-gotchas">Common gotchas</h3>
  548. <p>Now let’s simulate real process of learning and let’s make mistake in above script and try to debug it,
  549. this should be really helpful for demonstration purposes.</p>
  550. <p>This is how sample broken async function looks like:</p>
  551. <pre><code class="language-python" data-lang="python"><span class="c"># WARNING! BROKEN CODE DO NOT COPY PASTE</span>
  552. <span class="n">async</span> <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">url</span><span class="p">):</span>
  553. <span class="n">async</span> <span class="k">with</span> <span class="n">ClientSession</span><span class="p">()</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
  554. <span class="n">async</span> <span class="k">with</span> <span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">as</span> <span class="n">response</span><span class="p">:</span>
  555. <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">read</span><span class="p">()</span></code></pre>
  556. <p>This code is broken, but it’s not that easy to figure out why
  557. if you dont know much about asyncio. Even if you know Python well but you dont
  558. know asyncio or aiohttp well you’ll be in trouble to figure out what happens.</p>
  559. <p>What is output of above function?</p>
  560. <p>It produces following output:</p>
  561. <pre><code class="language-python" data-lang="python"><span class="n">pawel</span><span class="nd">@pawel</span><span class="o">-</span><span class="n">VPCEH390X</span> <span class="o">~/</span><span class="n">p</span><span class="o">/</span><span class="n">l</span><span class="o">/</span><span class="n">benchmarker</span><span class="o">&gt;</span> <span class="o">./</span><span class="n">bench</span><span class="o">.</span><span class="n">py</span>
  562. <span class="p">[</span><span class="o">&lt;</span><span class="n">generator</span> <span class="nb">object</span> <span class="n">ClientResponse</span><span class="o">.</span><span class="n">read</span> <span class="n">at</span> <span class="mh">0x7fa68d465728</span><span class="o">&gt;</span><span class="p">,</span> <span class="o">&lt;</span><span class="n">generator</span> <span class="nb">object</span> <span class="n">ClientResponse</span><span class="o">.</span><span class="n">read</span> <span class="n">at</span> <span class="mh">0x7fa68cdd9468</span><span class="o">&gt;</span><span class="p">,</span> <span class="o">&lt;</span><span class="n">generator</span> <span class="nb">object</span> <span class="n">ClientResponse</span><span class="o">.</span><span class="n">read</span> <span class="n">at</span> <span class="mh">0x7fa68d4656d0</span><span class="o">&gt;</span><span class="p">,</span> <span class="o">&lt;</span><span class="n">generator</span> <span class="nb">object</span> <span class="n">ClientResponse</span><span class="o">.</span><span class="n">read</span> <span class="n">at</span> <span class="mh">0x7fa68cdd9af0</span><span class="o">&gt;</span><span class="p">]</span></code></pre>
  563. <p>What happens here? You expected to get response objects after all processing is done, but here you actually get
  564. bunch of generators, why is that?</p>
  565. <p>It happens because as I’ve mentioned earlier <code class="highlighter-rouge">response.read()</code> is async
  566. operation, this means that it does not return result immediately, it just returns generator.
  567. This generator still needs to be called and
  568. executed, and this does not happen by default, <code class="highlighter-rouge">yield from</code> in Python 3.4 and <code class="highlighter-rouge">await</code> in Python 3.5 were
  569. added exactly for this purpose: to actually iterate over generator function. Fix to above error
  570. is just adding await before <code class="highlighter-rouge">response.read()</code>.</p>
  571. <pre><code class="language-python" data-lang="python"> <span class="c"># async operation must be preceded by await </span>
  572. <span class="k">return</span> <span class="n">await</span> <span class="n">response</span><span class="o">.</span><span class="n">read</span><span class="p">()</span> <span class="c"># NOT: return response.read()</span></code></pre>
  573. <p>Let’s break our code in some other way.</p>
  574. <pre><code class="language-python" data-lang="python"><span class="c"># WARNING! BROKEN CODE DO NOT COPY PASTE</span>
  575. <span class="n">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">loop</span><span class="p">,</span> <span class="n">r</span><span class="p">):</span>
  576. <span class="n">url</span> <span class="o">=</span> <span class="s">"http://localhost:8080/{}"</span>
  577. <span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>
  578. <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">r</span><span class="p">):</span>
  579. <span class="n">task</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">fetch</span><span class="p">(</span><span class="n">url</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">i</span><span class="p">)))</span>
  580. <span class="n">tasks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
  581. <span class="n">responses</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">)</span>
  582. <span class="k">print</span><span class="p">(</span><span class="n">responses</span><span class="p">)</span></code></pre>
  583. <p>Again above code is broken but it’s not easy to figure out why if you’re just
  584. learning asyncio.</p>
  585. <p>Above produces following output:</p>
  586. <pre><code class="language-python" data-lang="python"><span class="n">pawel</span><span class="nd">@pawel</span><span class="o">-</span><span class="n">VPCEH390X</span> <span class="o">~/</span><span class="n">p</span><span class="o">/</span><span class="n">l</span><span class="o">/</span><span class="n">benchmarker</span><span class="o">&gt;</span> <span class="o">./</span><span class="n">bench</span><span class="o">.</span><span class="n">py</span>
  587. <span class="o">&lt;</span><span class="n">_GatheringFuture</span> <span class="n">pending</span><span class="o">&gt;</span>
  588. <span class="n">Task</span> <span class="n">was</span> <span class="n">destroyed</span> <span class="n">but</span> <span class="n">it</span> <span class="ow">is</span> <span class="n">pending</span><span class="err">!</span>
  589. <span class="n">task</span><span class="p">:</span> <span class="o">&lt;</span><span class="n">Task</span> <span class="n">pending</span> <span class="n">coro</span><span class="o">=&lt;</span><span class="n">fetch</span><span class="p">()</span> <span class="n">running</span> <span class="n">at</span> <span class="o">./</span><span class="n">bench</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">7</span><span class="o">&gt;</span> <span class="n">wait_for</span><span class="o">=&lt;</span><span class="n">Future</span> <span class="n">pending</span> <span class="n">cb</span><span class="o">=</span><span class="p">[</span><span class="n">Task</span><span class="o">.</span><span class="n">_wakeup</span><span class="p">()]</span><span class="o">&gt;</span> <span class="n">cb</span><span class="o">=</span><span class="p">[</span><span class="n">gather</span><span class="o">.&lt;</span><span class="nb">locals</span><span class="o">&gt;.</span><span class="n">_done_callback</span><span class="p">(</span><span class="mi">0</span><span class="p">)()</span> <span class="n">at</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">python3</span><span class="o">.</span><span class="mi">5</span><span class="o">/</span><span class="n">asyncio</span><span class="o">/</span><span class="n">tasks</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">602</span><span class="p">]</span><span class="o">&gt;</span>
  590. <span class="n">Task</span> <span class="n">was</span> <span class="n">destroyed</span> <span class="n">but</span> <span class="n">it</span> <span class="ow">is</span> <span class="n">pending</span><span class="err">!</span>
  591. <span class="n">task</span><span class="p">:</span> <span class="o">&lt;</span><span class="n">Task</span> <span class="n">pending</span> <span class="n">coro</span><span class="o">=&lt;</span><span class="n">fetch</span><span class="p">()</span> <span class="n">running</span> <span class="n">at</span> <span class="o">./</span><span class="n">bench</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">7</span><span class="o">&gt;</span> <span class="n">wait_for</span><span class="o">=&lt;</span><span class="n">Future</span> <span class="n">pending</span> <span class="n">cb</span><span class="o">=</span><span class="p">[</span><span class="n">Task</span><span class="o">.</span><span class="n">_wakeup</span><span class="p">()]</span><span class="o">&gt;</span> <span class="n">cb</span><span class="o">=</span><span class="p">[</span><span class="n">gather</span><span class="o">.&lt;</span><span class="nb">locals</span><span class="o">&gt;.</span><span class="n">_done_callback</span><span class="p">(</span><span class="mi">1</span><span class="p">)()</span> <span class="n">at</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">python3</span><span class="o">.</span><span class="mi">5</span><span class="o">/</span><span class="n">asyncio</span><span class="o">/</span><span class="n">tasks</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">602</span><span class="p">]</span><span class="o">&gt;</span>
  592. <span class="n">Task</span> <span class="n">was</span> <span class="n">destroyed</span> <span class="n">but</span> <span class="n">it</span> <span class="ow">is</span> <span class="n">pending</span><span class="err">!</span>
  593. <span class="n">task</span><span class="p">:</span> <span class="o">&lt;</span><span class="n">Task</span> <span class="n">pending</span> <span class="n">coro</span><span class="o">=&lt;</span><span class="n">fetch</span><span class="p">()</span> <span class="n">running</span> <span class="n">at</span> <span class="o">./</span><span class="n">bench</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">7</span><span class="o">&gt;</span> <span class="n">wait_for</span><span class="o">=&lt;</span><span class="n">Future</span> <span class="n">pending</span> <span class="n">cb</span><span class="o">=</span><span class="p">[</span><span class="n">Task</span><span class="o">.</span><span class="n">_wakeup</span><span class="p">()]</span><span class="o">&gt;</span> <span class="n">cb</span><span class="o">=</span><span class="p">[</span><span class="n">gather</span><span class="o">.&lt;</span><span class="nb">locals</span><span class="o">&gt;.</span><span class="n">_done_callback</span><span class="p">(</span><span class="mi">2</span><span class="p">)()</span> <span class="n">at</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">python3</span><span class="o">.</span><span class="mi">5</span><span class="o">/</span><span class="n">asyncio</span><span class="o">/</span><span class="n">tasks</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">602</span><span class="p">]</span><span class="o">&gt;</span>
  594. <span class="n">Task</span> <span class="n">was</span> <span class="n">destroyed</span> <span class="n">but</span> <span class="n">it</span> <span class="ow">is</span> <span class="n">pending</span><span class="err">!</span>
  595. <span class="n">task</span><span class="p">:</span> <span class="o">&lt;</span><span class="n">Task</span> <span class="n">pending</span> <span class="n">coro</span><span class="o">=&lt;</span><span class="n">fetch</span><span class="p">()</span> <span class="n">running</span> <span class="n">at</span> <span class="o">./</span><span class="n">bench</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">7</span><span class="o">&gt;</span> <span class="n">wait_for</span><span class="o">=&lt;</span><span class="n">Future</span> <span class="n">pending</span> <span class="n">cb</span><span class="o">=</span><span class="p">[</span><span class="n">Task</span><span class="o">.</span><span class="n">_wakeup</span><span class="p">()]</span><span class="o">&gt;</span> <span class="n">cb</span><span class="o">=</span><span class="p">[</span><span class="n">gather</span><span class="o">.&lt;</span><span class="nb">locals</span><span class="o">&gt;.</span><span class="n">_done_callback</span><span class="p">(</span><span class="mi">3</span><span class="p">)()</span> <span class="n">at</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">python3</span><span class="o">.</span><span class="mi">5</span><span class="o">/</span><span class="n">asyncio</span><span class="o">/</span><span class="n">tasks</span><span class="o">.</span><span class="n">py</span><span class="p">:</span><span class="mi">602</span><span class="p">]</span><span class="o">&gt;</span></code></pre>
  596. <p>What happens here? If you examine your localhost logs you may see that requests are not reaching
  597. your server at all. Clearly no requests are performed. Print statement prints that
  598. responses variable contains <code class="highlighter-rouge">&lt;_GatheringFuture pending&gt;</code> object, and later it alerts that
  599. pending tasks were destroyed. Why is it happening? Again you forgot about await</p>
  600. <p>faulty line is this</p>
  601. <pre><code class="language-python" data-lang="python"> <span class="n">responses</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">)</span></code></pre>
  602. <p>it should be:</p>
  603. <pre><code class="language-python" data-lang="python"> <span class="n">responses</span> <span class="o">=</span> <span class="n">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">)</span></code></pre>
  604. <p>I guess main lesson from those mistakes is: always remember about using “await” if
  605. you’re actually awaiting something.</p>
  606. <h2 id="sync-vs-async">Sync vs Async</h2>
  607. <p>Finally time for some fun. Let’s check if async is really worth the hassle. What’s the
  608. difference in efficiency between asynchronous client and blocking client? How many
  609. requests per minute can I send with my async client?</p>
  610. <p>With this questions in mind I set up simple (async) aiohttp server.
  611. My server is going to read full html text of Frankenstein by Marry Shelley. It will
  612. add random delays between responses. Some responses will have zero delay, and some will
  613. have maximum of 3 seconds delay. This should resemble real applications, few
  614. apps respond to all requests with same latency, usually latency differs
  615. from response to response.</p>
  616. <p>Server code looks like this:</p>
  617. <pre><code class="language-python" data-lang="python"><span class="c">#!/usr/local/bin/python3.5</span>
  618. <span class="kn">import</span> <span class="nn">asyncio</span>
  619. <span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
  620. <span class="kn">from</span> <span class="nn">aiohttp</span> <span class="kn">import</span> <span class="n">web</span>
  621. <span class="kn">import</span> <span class="nn">random</span>
  622. <span class="c"># set seed to ensure async and sync client get same distribution of delay values</span>
  623. <span class="c"># and tests are fair</span>
  624. <span class="n">random</span><span class="o">.</span><span class="n">seed</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
  625. <span class="n">async</span> <span class="k">def</span> <span class="nf">hello</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
  626. <span class="n">name</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">match_info</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">"name"</span><span class="p">,</span> <span class="s">"foo"</span><span class="p">)</span>
  627. <span class="n">n</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
  628. <span class="n">delay</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
  629. <span class="n">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">delay</span><span class="p">)</span>
  630. <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s">"content_type"</span><span class="p">:</span> <span class="s">"text/html"</span><span class="p">,</span> <span class="s">"delay"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">delay</span><span class="p">)}</span>
  631. <span class="c"># opening file is not async here, so it may block, to improve</span>
  632. <span class="c"># efficiency of this you can consider using asyncio Executors</span>
  633. <span class="c"># that will delegate file operation to separate thread or process</span>
  634. <span class="c"># and improve performance</span>
  635. <span class="c"># https://docs.python.org/3/library/asyncio-eventloop.html#executor</span>
  636. <span class="c"># https://pymotw.com/3/asyncio/executors.html</span>
  637. <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"frank.html"</span><span class="p">,</span> <span class="s">"rb"</span><span class="p">)</span> <span class="k">as</span> <span class="n">html_body</span><span class="p">:</span>
  638. <span class="k">print</span><span class="p">(</span><span class="s">"{}: {} delay: {}"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">request</span><span class="o">.</span><span class="n">path</span><span class="p">,</span> <span class="n">delay</span><span class="p">))</span>
  639. <span class="n">response</span> <span class="o">=</span> <span class="n">web</span><span class="o">.</span><span class="n">Response</span><span class="p">(</span><span class="n">body</span><span class="o">=</span><span class="n">html_body</span><span class="o">.</span><span class="n">read</span><span class="p">(),</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span>
  640. <span class="k">return</span> <span class="n">response</span>
  641. <span class="n">app</span> <span class="o">=</span> <span class="n">web</span><span class="o">.</span><span class="n">Application</span><span class="p">()</span>
  642. <span class="n">app</span><span class="o">.</span><span class="n">router</span><span class="o">.</span><span class="n">add_route</span><span class="p">(</span><span class="s">"GET"</span><span class="p">,</span> <span class="s">"/{name}"</span><span class="p">,</span> <span class="n">hello</span><span class="p">)</span>
  643. <span class="n">web</span><span class="o">.</span><span class="n">run_app</span><span class="p">(</span><span class="n">app</span><span class="p">)</span></code></pre>
  644. <p>Synchronous client looks like this:</p>
  645. <pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="nn">requests</span>
  646. <span class="n">r</span> <span class="o">=</span> <span class="mi">100</span>
  647. <span class="n">url</span> <span class="o">=</span> <span class="s">"http://localhost:8080/{}"</span>
  648. <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">r</span><span class="p">):</span>
  649. <span class="n">res</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">i</span><span class="p">))</span>
  650. <span class="n">delay</span> <span class="o">=</span> <span class="n">res</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">"DELAY"</span><span class="p">)</span>
  651. <span class="n">d</span> <span class="o">=</span> <span class="n">res</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">"DATE"</span><span class="p">)</span>
  652. <span class="k">print</span><span class="p">(</span><span class="s">"{}:{} delay {}"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="n">res</span><span class="o">.</span><span class="n">url</span><span class="p">,</span> <span class="n">delay</span><span class="p">))</span></code></pre>
  653. <p>How long will it take to run this?</p>
  654. <p>On my machine running above synchronous client took 2:45.54 minutes.</p>
  655. <p>My async code looks just like above code samples above. How long will async client take?</p>
  656. <p>On my machine it took 0:03.48 seconds.</p>
  657. <p>It is interesting that it took exactly as long as longest delay
  658. from my server. If you look into messages printed by client script you can see how
  659. great async HTTP client is. Some responses had 0 delay but others got 3 seconds delay. In synchronous client
  660. they would be blocking and waiting, your machine would simply stay idle for this time.
  661. Async client does not waste time, when something is delayed it simply does
  662. something else, issues other requests or processes all other responses. You can see this clearly in logs, first there
  663. are responses with 0 delay, then after they arrrived you can see responses with 1 seconds delay,
  664. and so on until most delayed responses arrive.</p>
  665. <h2 id="testing-the-limits">Testing the limits</h2>
  666. <p>Now that we know our async client is better let’s try to test its limits and try to crash our
  667. localhost. I’m going to start with sending 1k async requests. I’m curious how many requests
  668. my client can handle.</p>
  669. <pre><code class="language-bash" data-lang="bash"><span class="gp">&gt; </span><span class="nb">time </span>python3 bench.py
  670. 2.68user 0.24system 0:07.14elapsed 40%CPU <span class="o">(</span>0avgtext+0avgdata 53704maxresident<span class="o">)</span>k
  671. 0inputs+0outputs <span class="o">(</span>0major+14156minor<span class="o">)</span>pagefaults 0swaps</code></pre>
  672. <p>So 1k requests take 7 seconds, pretty nice! How about 10k? Trying to make 10k requests
  673. unfortunately fails…</p>
  674. <pre><code class="language-python" data-lang="python"><span class="n">responses</span> <span class="n">are</span> <span class="o">&lt;</span><span class="n">_GatheringFuture</span> <span class="n">finished</span> <span class="n">exception</span><span class="o">=</span><span class="n">ClientOSError</span><span class="p">(</span><span class="mi">24</span><span class="p">,</span> <span class="s">'Cannot connect to host localhost:8080 ssl:False [Can not connect to localhost:8080 [Too many open files]]'</span><span class="p">)</span><span class="o">&gt;</span>
  675. <span class="n">Traceback</span> <span class="p">(</span><span class="n">most</span> <span class="n">recent</span> <span class="n">call</span> <span class="n">last</span><span class="p">):</span>
  676. <span class="n">File</span> <span class="s">"/home/pawel/.local/lib/python3.5/site-packages/aiohttp/connector.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">581</span><span class="p">,</span> <span class="ow">in</span> <span class="n">_create_connection</span>
  677. <span class="n">File</span> <span class="s">"/usr/local/lib/python3.5/asyncio/base_events.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">651</span><span class="p">,</span> <span class="ow">in</span> <span class="n">create_connection</span>
  678. <span class="n">File</span> <span class="s">"/usr/local/lib/python3.5/asyncio/base_events.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">618</span><span class="p">,</span> <span class="ow">in</span> <span class="n">create_connection</span>
  679. <span class="n">File</span> <span class="s">"/usr/local/lib/python3.5/socket.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">134</span><span class="p">,</span> <span class="ow">in</span> <span class="n">__init__</span>
  680. <span class="nb">OSError</span><span class="p">:</span> <span class="p">[</span><span class="n">Errno</span> <span class="mi">24</span><span class="p">]</span> <span class="n">Too</span> <span class="n">many</span> <span class="nb">open</span> <span class="n">files</span></code></pre>
  681. <p>That’s bad, seems like I stumbled across <a href="http://www.webcitation.org/6ICibHuyd">10k connections problem</a>.</p>
  682. <p>It says “too many open files”, and probably refers to number of open sockets.
  683. Why does it call them files? Sockets are just file descriptors, operating systems limit number of open sockets
  684. allowed. How many files are too many? I checked with python resource module and it seems like it’s around 1024.
  685. How can we bypass this? Primitive way is just increasing limit of open files. But this
  686. is probably not the good way to go. Much better way is just adding some synchronization
  687. in your client limiting number of concurrent requests it can process. I’m going to do this
  688. by adding <a href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Semaphore"><code class="highlighter-rouge">asyncio.Semaphore()</code></a> with max tasks of 1000.</p>
  689. <p>Modified client code looks like this now:</p>
  690. <pre><code class="language-python" data-lang="python"><span class="c"># modified fetch function with semaphore</span>
  691. <span class="kn">import</span> <span class="nn">random</span>
  692. <span class="kn">import</span> <span class="nn">asyncio</span>
  693. <span class="kn">from</span> <span class="nn">aiohttp</span> <span class="kn">import</span> <span class="n">ClientSession</span>
  694. <span class="n">async</span> <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">url</span><span class="p">):</span>
  695. <span class="n">async</span> <span class="k">with</span> <span class="n">ClientSession</span><span class="p">()</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
  696. <span class="n">async</span> <span class="k">with</span> <span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">as</span> <span class="n">response</span><span class="p">:</span>
  697. <span class="n">delay</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">"DELAY"</span><span class="p">)</span>
  698. <span class="n">date</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">"DATE"</span><span class="p">)</span>
  699. <span class="k">print</span><span class="p">(</span><span class="s">"{}:{} with delay {}"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">date</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">url</span><span class="p">,</span> <span class="n">delay</span><span class="p">))</span>
  700. <span class="k">return</span> <span class="n">await</span> <span class="n">response</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
  701. <span class="n">async</span> <span class="k">def</span> <span class="nf">bound_fetch</span><span class="p">(</span><span class="n">sem</span><span class="p">,</span> <span class="n">url</span><span class="p">):</span>
  702. <span class="c"># getter function with semaphore</span>
  703. <span class="n">async</span> <span class="k">with</span> <span class="n">sem</span><span class="p">:</span>
  704. <span class="n">await</span> <span class="n">fetch</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
  705. <span class="n">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">loop</span><span class="p">,</span> <span class="n">r</span><span class="p">):</span>
  706. <span class="n">url</span> <span class="o">=</span> <span class="s">"http://localhost:8080/{}"</span>
  707. <span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>
  708. <span class="c"># create instance of Semaphore</span>
  709. <span class="n">sem</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Semaphore</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span>
  710. <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">r</span><span class="p">):</span>
  711. <span class="c"># pass Semaphore to every GET request</span>
  712. <span class="n">task</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">bound_fetch</span><span class="p">(</span><span class="n">sem</span><span class="p">,</span> <span class="n">url</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">i</span><span class="p">)))</span>
  713. <span class="n">tasks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
  714. <span class="n">responses</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">)</span>
  715. <span class="n">await</span> <span class="n">responses</span>
  716. <span class="n">number</span> <span class="o">=</span> <span class="mi">10000</span>
  717. <span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_event_loop</span><span class="p">()</span>
  718. <span class="n">future</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">run</span><span class="p">(</span><span class="n">loop</span><span class="p">,</span> <span class="n">number</span><span class="p">))</span>
  719. <span class="n">loop</span><span class="o">.</span><span class="n">run_until_complete</span><span class="p">(</span><span class="n">future</span><span class="p">)</span></code></pre>
  720. <p>At this point I can process 10k urls. It takes 23 seconds and returns some exceptions but overall
  721. it’s pretty nice!</p>
  722. <p>How about 100 000? This really makes my computer work hard but suprisingly
  723. it works ok. Server turns out to be suprisingly stable although
  724. you can see that ram usage gets pretty high at this point, cpu usage is around
  725. 100% all the time. What I find interesting is that my server takes significantly less cpu than client.
  726. Here’s snapshot of linux <code class="highlighter-rouge">ps</code> output.</p>
  727. <pre><code class="language-python" data-lang="python"><span class="n">pawel</span><span class="nd">@pawel</span><span class="o">-</span><span class="n">VPCEH390X</span> <span class="o">~/</span><span class="n">p</span><span class="o">/</span><span class="n">l</span><span class="o">/</span><span class="n">benchmarker</span><span class="o">&gt;</span> <span class="n">ps</span> <span class="n">ua</span> <span class="o">|</span> <span class="n">grep</span> <span class="n">python</span>
  728. <span class="n">USER</span> <span class="n">PID</span> <span class="o">%</span><span class="n">CPU</span> <span class="o">%</span><span class="n">MEM</span> <span class="n">VSZ</span> <span class="n">RSS</span> <span class="n">TTY</span> <span class="n">STAT</span> <span class="n">START</span> <span class="n">TIME</span> <span class="n">COMMAND</span>
  729. <span class="n">pawel</span> <span class="mi">2447</span> <span class="mf">56.3</span> <span class="mf">1.0</span> <span class="mi">216124</span> <span class="mi">64976</span> <span class="n">pts</span><span class="o">/</span><span class="mi">9</span> <span class="n">Sl</span><span class="o">+</span> <span class="mi">21</span><span class="p">:</span><span class="mi">26</span> <span class="mi">1</span><span class="p">:</span><span class="mi">27</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="nb">bin</span><span class="o">/</span><span class="n">python3</span><span class="o">.</span><span class="mi">5</span> <span class="o">./</span><span class="n">test_server</span><span class="o">.</span><span class="n">py</span>
  730. <span class="n">pawel</span> <span class="mi">2527</span> <span class="mi">101</span> <span class="mf">3.5</span> <span class="mi">674732</span> <span class="mi">212076</span> <span class="n">pts</span><span class="o">/</span><span class="mi">0</span> <span class="n">Rl</span><span class="o">+</span> <span class="mi">21</span><span class="p">:</span><span class="mi">26</span> <span class="mi">2</span><span class="p">:</span><span class="mi">30</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="nb">bin</span><span class="o">/</span><span class="n">python3</span><span class="o">.</span><span class="mi">5</span> <span class="o">./</span><span class="n">bench</span><span class="o">.</span><span class="n">py</span></code></pre>
  731. <p>Overall it took around 5 minutes before it crashed for some
  732. reason. It generated around 100k lines of output so it’s not that easy
  733. to pinpoint traceback, seems like some responses are not closed, is it
  734. because of some error from my server or something in client?</p>
  735. <p>After scrolling for couple of seconds I found this exception in client logs.</p>
  736. <pre><code class="language-python" data-lang="python"> <span class="n">File</span> <span class="s">"/usr/local/lib/python3.5/asyncio/futures.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">387</span><span class="p">,</span> <span class="ow">in</span> <span class="n">__iter__</span>
  737. <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">result</span><span class="p">()</span> <span class="c"># May raise too.</span>
  738. <span class="n">File</span> <span class="s">"/usr/local/lib/python3.5/asyncio/futures.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">274</span><span class="p">,</span> <span class="ow">in</span> <span class="n">result</span>
  739. <span class="k">raise</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exception</span>
  740. <span class="n">File</span> <span class="s">"/usr/local/lib/python3.5/asyncio/selector_events.py"</span><span class="p">,</span> <span class="n">line</span> <span class="mi">411</span><span class="p">,</span> <span class="ow">in</span> <span class="n">_sock_connect</span>
  741. <span class="n">sock</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">address</span><span class="p">)</span>
  742. <span class="nb">OSError</span><span class="p">:</span> <span class="p">[</span><span class="n">Errno</span> <span class="mi">99</span><span class="p">]</span> <span class="n">Cannot</span> <span class="n">assign</span> <span class="n">requested</span> <span class="n">address</span></code></pre>
  743. <p>I dont really know what happens here. My initial hypothesis is that test server went down for some split second,
  744. and this caused some client error that was printed at the end. <a href="https://news.ycombinator.com/item?id=11557672">One of the readers</a>
  745. suggests that this exception may be caused by OS running out of free ephemereal ports. I added semaphore
  746. earlier so number of concurrent connections should be maximum 1k, but some sockets may still be
  747. closing and not available for kernel to assign.</p>
  748. <p>Overall it’s really not bad, 5 minutes for 100 000 requests? This makes around 20k
  749. requests per minute. Pretty powerful if you ask me.</p>
  750. <p>Finally I’m going to try 1 million requests. I really hope my laptop is not going
  751. to explode when testing that. For this amount of requests I reduced delays from server to range between 0 and 1.</p>
  752. <p>1 000 000 requests finished in 52 minutes</p>
  753. <pre><code class="language-bash" data-lang="bash">1913.06user 1196.09system 52:06.87elapsed 99%CPU <span class="o">(</span>0avgtext+0avgdata 5194260maxresident<span class="o">)</span>k
  754. 265144inputs+0outputs <span class="o">(</span>18692major+2528207minor<span class="o">)</span>pagefaults 0swaps</code></pre>
  755. <p>so it means my client made around 19230 requests per minute. Not bad isn’t it? Note that
  756. capabilities of my client are limited by server responding with delay of 0 and 1 in this
  757. scenario, seems like my test server also crashed silently couple of times.</p>
  758. <h2 id="epilogue">Epilogue</h2>
  759. <p>You can see that asynchronous HTTP clients can be pretty powerful. Performing
  760. 1 million requests from async client is not difficult, and the client performs really well in comparison
  761. to synchronous code.</p>
  762. <p>I wonder how it compares to other languages and async frameworks? Perhaps in some
  763. future post I could compare <a href="https://github.com/twisted/treq">Twisted Treq</a> with
  764. aiohttp. There is also question how many concurrent requests can be issued by
  765. async libraries in other languages. E.g. what would be results of benchmarks
  766. for some Java async frameworks? Or C++ frameworks? Or some Rust HTTP clients?</p>
  767. <h3 id="edits-24042016"><em>EDITS (24/04/2016)</em></h3>
  768. <ul>
  769. <li>improved code sample that uses Semaphore</li>
  770. <li>added comment about using executor when opening file</li>
  771. <li>added link to HN comment about EADDRNOTAVAIL exception</li>
  772. </ul>
  773. </article>
  774. </section>
  775. <nav id="jumpto">
  776. <p>
  777. <a href="/david/blog/">Accueil du blog</a> |
  778. <a href="http://pawelmhm.github.io/asyncio/python/aiohttp/2016/04/22/asyncio-aiohttp.html">Source originale</a> |
  779. <a href="/david/stream/2019/">Accueil du flux</a>
  780. </p>
  781. </nav>
  782. <footer>
  783. <div>
  784. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  785. <p>
  786. Bonjour/Hi!
  787. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  788. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  789. </p>
  790. <p>
  791. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  792. </p>
  793. <p>
  794. Voici quelques articles choisis :
  795. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  796. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  797. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  798. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  799. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  800. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  801. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  802. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  803. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  804. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  805. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  806. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  807. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  808. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  809. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  810. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  811. </p>
  812. <p>
  813. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  814. </p>
  815. <p>
  816. Je ne traque pas ta navigation mais mon
  817. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  818. conserve des logs d’accès.
  819. </p>
  820. </div>
  821. </footer>
  822. <script type="text/javascript">
  823. ;(_ => {
  824. const jumper = document.getElementById('jumper')
  825. jumper.addEventListener('click', e => {
  826. e.preventDefault()
  827. const anchor = e.target.getAttribute('href')
  828. const targetEl = document.getElementById(anchor.substring(1))
  829. targetEl.scrollIntoView({behavior: 'smooth'})
  830. })
  831. })()
  832. </script>