Node Performance Measurement

I was working on optimising a javascript maths function and wanted to compare the performance of different versions of the code. Initially I had some difficulty because I was approaching it incorrectly so I wanted to make a note for future reference.

First mistake – only using a small number of runs. I was comparing the two different functions with a very small iteration amount. This was leading to weird results where each iteration would get faster even though it was actually the same code running. I think this was related to some compiler optimisation or something. Anyway, after reading JavaScript Compiler Optimization Techniques, I changed to a very large number of runs. This made the results much more consistent.

Second mistake – using perf_hooks incorrectly. From the same blog I also found out the nice way to use perf_hooks to measure the performance:`

import { performance, PerformanceObserver } from 'perf_hooks';

let iterations = 1_000_000;

performance.mark('start');
while (iterations--) {
    StableMath._calcOutGivenIn(
        poolPairDataBigInt.amp,
        poolPairDataBigInt.balances,
        poolPairDataBigInt.tokenIndexIn,
        poolPairDataBigInt.tokenIndexOut,
        amt,
        poolPairDataBigInt.fee
    );
}
performance.mark('end');

iterations = 1_000_000;

performance.mark('startSecond');
const invariant = StableMath._calculateInvariant(
    poolPairDataBigInt.amp,
    poolPairDataBigInt.balances,
    true
);
while (iterations--) {
    StableMath._calcOutGivenInNoInv(
        poolPairDataBigInt.amp,
        poolPairDataBigInt.balances,
        poolPairDataBigInt.tokenIndexIn,
        poolPairDataBigInt.tokenIndexOut,
        amt,
        poolPairDataBigInt.fee,
        invariant
    );
}
performance.mark('endSecond');

const obs = new PerformanceObserver((list, observer) => {
    console.log(list.getEntries()); // [0]);
    performance.clearMarks();
    observer.disconnect();
});
obs.observe({ entryTypes: ['measure'] });

performance.measure('NoOptimisation', 'start', 'end');
performance.measure('WithOptimisation', 'startSecond', 'endSecond');

Which results in an output like:

[
  PerformanceMeasure {
    name: 'NoOptimisation',
    entryType: 'measure',
    startTime: 2369.287365913391,
    duration: 35891.85489702225,
    detail: null
  },
  PerformanceMeasure {
    name: 'WithOptimisation',
    entryType: 'measure',
    startTime: 38261.19673395157,
    duration: 18529.005373954773,
    detail: null
  }
]

As well as having a nice output it also shows my optimisation worked pretty nicely!

Photo by Saffu on Unsplash

Forking Brilliant

Houston We Have A Problem

I want to check that a transaction will work on mainnet for an account I don’t control. In this case it’s for a large LP in the Balancer staBal3 pool and I want to check they could successfully migrates their staBal3 to the new bb-a-USD using a Relayer multicall with the params created by the SDK.

This definitely isn’t the most elegant way of doing things but it works!

Whale Hunting

The first thing I need to do is to find a large staBal3 LP account and figure out their BPT balance. I can use the Balancer Subgraph to query account pool shares for the staBal3 pool. Query looks like:

query MyQuery {
  poolShares(where: {poolId: "0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063"}, orderBy: balance, orderDirection: desc) {
    id
    balance
  }
}

I then manually worked my way down the list of addresses until I found one that was an EOA: https://etherscan.io/address/0x4086e3e1e99a563989a9390facff553a4f29b6ee and at the time of investiagation this had a BPT balance of: 10205792.037653741889318463 (a cool $10.28mil!).

Exit Stage Right

The SDK helper function (relayer.exitPoolAndBatchSwap) that creates the call data requires an input param of expectedAmountsOut which in this case represents the DAI/USDC/USDT amounts out when exiting the staBal3 pool. Because I don’t have the maths required for this exposed yet a quick way to get this is to see the output amounts using Withdraw in the UI. There’s a very nice tool that allows us to simulate this when we don’t have control of the account of interest: https://www.impersonator.xyz/

Now that I’ve got all the info required I can generate the call data by using the helper function. In this case we get an array of call data which represent an exitPool call on staBal3 followed by a batchSwap that swaps the stables received from the exit to the bb-a-USD BPT.

The Magic

Tenderly has lots of useful features including Transaction Simulations. To begin I tried simulating the multicall call on the Mainnet Relayer but the tx failed highlighting a new issue – the account hasn’t approved the Balancer Relayer. To get around this I can use a Tenderly Fork – “Forks allow you to chain simulation and test out complex scenarios with live on-chain data”. This is cool because I can now fork the chain make an approval on the relayer from the account which then allows me to succesfully simulate the multicall!

Photo by Joel Muniz on Unsplash

Building an SDK 0.1.0 – Improving SOR data sourcing

Intro

A big focus for Balancer Labs this year is to make it really easy to build on top of the protocol. To aid in that we’re putting together the `@balancer-labs/sdk npm package. As the current lead in this project I thought I’d try and document some of the work to help keep track of the changes, thought process and learning along the way. It’ll also be useful as a reminder of what’s going on!

SOR v2

Some background

We already have the Smart Order Router (@balancer-labs/sor), a package that devs can use to source the optimal routing for a swap using Balancer liquidity. It’s used in Balancers front-end and other projects like Copper and is a solver for Gnosis BGP. It’s also used in the Beethoven front-end (a Balancer friendly fork on Fantom, cool project and team and worth checking out).

The SOR is also used and exposed by the SDK. It’s core to making swaps accesible but is also used for joining/exiting Boosted Pools which uses PhantomBpt and swaps (a topic for another time I think!).

SOR Data

The diagram below shows some of the core parts of the SOR v2.

SOR v2

To choose the optimal routes for a swap the SOR needs information about the Balancer pools and the price of assets. And as we can see from the diagram the sourcing of this data is currently very tightly coupled to the SOR. Pools data is retrieved from the Subgraph and updated with on-chain balances using a multicall. And asset pricing is retrieved from CoinGecko.

Recently Beethoven experienced a pretty large growth spurt and found there were some major issues retrieving data from the Subgraph. They also correctly pointed out that CoinGecko doesn’t always have the asset pricing (especially on Fantom) and this information could be available from other sources.

After some discussions with Daniel (a very helpful dev from Beethoven) it was agreed that a good approach would be to refactor the SOR to create composability of data fetching so the user is able to have more control over where data is coming from. With this approach, the SOR doesn’t need to know anything about CoinGecko or the Subgraph and the data could now come from anywhere (database, cache, on chain, etc.), and as long as it implements the interface, the SOR will work properly.

Changes – SOR v3

I came back from Christmas break and Daniel had made all the changes – friendly forks for the win ๐Ÿ’ช! The interface changes are breaking but the improvements are worth it – SOR 3.0.0.

Config

The goal was to remove all the chain specific config from the SOR and pass it in as a constructor parameter. This helps to avoid non-scalable hard-coded values and encorages a single source of truth. It also gives more flexibility for the variables and makes the code easier to test.

There is now the SorConfig type:

export interface SorConfig {
    chainId: number;
    weth: string;
    vault: string;
    staBal3Pool?: { id: string; address: string };
    wethStaBal3?: { id: string; address: string };
    usdcConnectingPool?: { id: string; usdc: string };
}

Pool Data

The goal here is to allow for flexibility in defining where the pool data is fetched from. We define a generic PoolDataService that has a single function getPools, which serves as a generic interface for fetching pool data. This allows allow for any number of custom services to be used without having to change anything in the SOR or SDK.

export interface PoolDataService {
    getPools(): Promise<SubgraphPoolBase[]>;
}

Approaching it this way means all the Subgraph and on-chain/multicall fetching logic is removed from the SOR. These will be added to the Balancer SDK as stand-alone services. But as a simple example this is a PoolDataService that retrieves data from Subgraph:

export class SubgraphPoolDataService implements PoolDataService {
    constructor(
        private readonly chainId: number,
        private readonly subgraphUrl: string
    ) {}

    public async getPools(): Promise<SubgraphPoolBase[]> {
        const response = await fetch(this.subgraphUrl, {
            method: 'POST',
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ query: Query[this.chainId] }),
        });

        const { data } = await response.json();

        return data.pools ?? [];
    }
}

Asset Pricing

The goal here is to allow for flexibility in defining where token prices are fetched from. We define a generic TokenPriceService that has a single function getNativeAssetPriceInToken. Similar to the PoolDataService this offers flexibility in the service that can be used, i.e. CoingeckoTokenPriceService or SubgraphTokenPriceService.

export interface TokenPriceService {
    /**
     * This should return the price of the native asset (ETH) in the token defined by tokenAddress.
     * Example: BAL = $20 USD, ETH = $4,000 USD, then 1 ETH = 200 BAL. This function would return 200.
     * @param tokenAddress
     */
    getNativeAssetPriceInToken(tokenAddress: string): Promise<string>;
}

All the CoinGecko code is removed from the SOR (to be added to SDK). An example TokenPriceService using CoinGecko:

export class CoingeckoTokenPriceService implements TokenPriceService {
    constructor(private readonly chainId: number) {}

    public async getNativeAssetPriceInToken(
        tokenAddress: string
    ): Promise<string> {
        const ethPerToken = await this.getTokenPriceInNativeAsset(tokenAddress);

        // We get the price of token in terms of ETH
        // We want the price of 1 ETH in terms of the token base units
        return `${1 / parseFloat(ethPerToken)}`;
    }

    /**
     * @dev Assumes that the native asset has 18 decimals
     * @param tokenAddress - the address of the token contract
     * @returns the price of 1 ETH in terms of the token base units
     */
    async getTokenPriceInNativeAsset(tokenAddress: string): Promise<string> {
        const endpoint = `https://api.coingecko.com/api/v3/simple/token_price/${this.platformId}?contract_addresses=${tokenAddress}&vs_currencies=${this.nativeAssetId}`;

        const response = await fetch(endpoint, {
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
            },
        });

        const data = await response.json();

        if (
            data[tokenAddress.toLowerCase()][this.nativeAssetId] === undefined
        ) {
            throw Error('No price returned from Coingecko');
        }

        return data[tokenAddress.toLowerCase()][this.nativeAssetId];
    }

    private get platformId(): string {
        switch (this.chainId) {
            case 1:
                return 'ethereum';
            case 42:
                return 'ethereum';
            case 137:
                return 'polygon-pos';
            case 42161:
                return 'arbitrum-one';
        }

        return '2';
    }

    private get nativeAssetId(): string {
        switch (this.chainId) {
            case 1:
                return 'eth';
            case 42:
                return 'eth';
            case 137:
                return '';
            case 42161:
                return 'eth';
        }

        return '';
    }
}

Final Outcome

After the changes the updated diagram shows how the SOR is more focused and less opinionated:

The plan for the Balancer front-end is to move away from using the SOR directly and use it via the SDK package. The SDK will have the data fetching functionality as serparate services (which can be used independetly for fetching pools, etc) and these will be passed to the SOR when the SDK is instantiated. BUT it’s also possible to use the SOR independendtly as shown in this swapExample.

This was a large and breaking change but with the continued issues with Subgraph and more teams using the SOR/SDK it was a neccessary upgrade. Many thanks to Daniel from the Beethoven team for pushing this through!

SITIR 10

โ‚ฟ A from-scratch tour of Bitcoin in Python – create, digitally sign, and broadcast a Bitcoin transaction in pure Python, from scratch, and with zero dependencies.

Computer scientist Leonard Adleman on – Choosing the right problem to work on ๐Ÿง˜

๐Ÿง  Booby Trapping the Ethereum Blockchain – Samczun again, amazing.

How to Die with Zero – This book is not about making your money grow โ€” itโ€™s about making your life grow ๐Ÿค”

๐Ÿงฐ Local ERC20 Balance Manipulation (with HardHat) – unlocks the ability to manipulate the state of any forked contract.

Capitalism: You Wouldn’t Trade it for Anything – Interesting thoughts ๐Ÿ’ก

๐Ÿ‹๏ธโ€โ™‚๏ธ How Much Ya Bench? Strength Benchmarks for Men

Automating Code Changes via GitHub Actions Making Pull Requests – Could be handy some time ๐Ÿฆฟ

๐Ÿ› ๏ธ Smok – Contract mocking in JavaScript

๐Ÿคช Pretty wacky story about Ian Freeman that’s worth a read

Eye opening look at defi “bluechip” performance over last year, denominated in ETH ๐Ÿ˜ฒ

๐Ÿ› Debugging the Comp hack with Dapptools

Punk6529 on Making It ๐Ÿ˜Ž

Thought provoking reasons to retire as soon as possible from this post by esimoney ๐Ÿ’”

Unbeatable Video Games – I like this and it echoes thoughts I had after watching Competitive Dog Grooming and I kind of wish I had something like that ๐Ÿ’ญ

A depth year is a cool idea! ๐Ÿ‘ท

Photo by Kelly Sikkema on Unsplash

You Can Do It!

At Balancer we recently finished a pretty major release and it was probably the most stressful release Iโ€™ve been involved in with there so far. I wanted to write this as a kind of log of a cool thing I helped build but also as a reminder that its possible to get through the tough times and come succesfully out the other side!

The release was actually two pretty major things – first there was the launch of Balancers MetaStable pools, and second was a partnership with Lido. MetaStable pools allow pools of tokens with highly correlated prices, for example cDAI and DAI and Lido were making use of a MetaStable pool to launch an incentivised WETH/wstETH pool. We integrated Lido pretty deeply and the goal was to support trades in stETH by using a Relayer contract to wrap/unwrap automatically, this meant we also had to take into account the stETH/wstETH exchange rates for calcualtions and display. We also wanted to make use of the deep WETH/Stable liquidity Balancer has and allow stable <> Lido trades, this was a slight issue because it involved a two hop trade (i.e. USDC > DAI > WETH > wstETH) which the SOR doesn’t currently support so we had to add static routes for those.

There was a lot going on across all the different teams in Balancer with some fairly tight/strict deadlines. At the time (and actually still now) there’s no Product Manager at Balancer so there wasn’t really someone managing the project. Because of this it very much felt like cat herding and some of the communication could have been better. Also things were changing a lot quite close to the deadline which made things pretty stressful for someone like me who likes to try and get things done early! The last mistake was the deployments of everything happened over the weekend for a Monday launch which wasn’t ideal. These are all things that are being addressed now that we’ve had some retrospectives – Balancer always seem good at continuously trying to improve.

A lot of this probably won’t make a great deal of sense but some of the things that I touched during the work:

  • SOR Hardcoded paths
  • SOR MarketSP support
  • Swap amount via query
  • Stable pool issues due to invariant
  • Subgraph update and rollout
  • Kovan test deployment:
    • stETH + wstETH token on Kovan
    • Faucet for wstETH
    • Correct pools
    • Relayer
  • Relayer – authorisation and approval debug
  • Relayer join/exits being canned after I spent time on them,
  • Limits on front-end
  • Front-end using correct stETH/wstETH exchange

And there was a whole bunch of other stuff being worked on across other teams.

And at the end of it all we made it! The launch went ahead smoothly and on time. The Lido pools are already some of the largest in the Balancer Vault and there haven’t been any major issues so far. I certainly learned a lot and enjoyed working with some team members I haven’t been involved with before. And although it was really stressful and tough at times, the old cliche of breaking down something large into smaller pieces and just working through them is true. Phew!

Update:

Kristen, the Balancer COO, posted a tweet about Impostor Syndrome from Just Kan that I thought was pretty pertinent.

“You will always feel like you are drowning. Even when you are succeeding, it feels like you are drowning because you are constantly being forced to do something new that you haven’t had experience in. While figuring this stuff out on the fly, you’re going to feel like you’re failing and that you’re an impostor.”

Photo by Ava Sol on Unsplash

SITIR 9

๐Ÿ˜ฝ Example of a Flashbot bundle to rescure a cryptokitty from a compromised wallet.

๐Ÿ’ก Another UMA post – they keep posting interesting use cases that I’m always interested to dive into but never have the time to do (I think this is maybe part of their problem). Keeping it logged here as a bookmark!

Really good advice (especially given the latest massive correction and volatility) from Ari Paul ๐Ÿฆ‰

I rarely have good ideas. To overcome this limitation, I think about one topic (like habits) for an unreasonable amount of time. Then, I revise, revise, revise until only the best stuff remains. Itโ€™s slow, but it works. You can either be a genius or you can be patient.

James Clear

๐Ÿ“ท Using a camera and TinyML to read a physical meter and convert it into an API.

๐Ÿ”ง Multicall pattern in Solidity using Delegatecall. Tweet from Austin.

๐Ÿ‘จโ€๐Ÿ’ป Interesting details about running your own ETH node in the cloud.

๐Ÿ˜Ž And this seems like something useful you could do with a node – eth_call any contract with an arbitrary code to access storage values without corresponding public views.

๐Ÿ‘ฎ Why I Did Not Go To Jail by Ben Horowitz – just a good example of doing the right, difficult thing.

โš–๏ธ A cool write up explaining what we’ve been working on at Balancer for V2.

โญ Not sure if this description of path to fame and fortune within Ethereum is meant to have a negative connotation but I’m on step 2!

๐Ÿคฃ Marshawn Lynch at Applebee’s – funny! But I like this guys attitude toward money from what I’ve read.

๐Ÿงฐ What problems do people solve with strace? – Could be handy some day.

๐Ÿง  This was cool – post by samczsun giving a guided walkthrough for swap, the hardest challenge in Paradigm CTF 2021.

๐Ÿงฎ I like what @adlrocha is doing with the Improving my math intuition series and the first part about Network Applications of Bloom Filters was really interesting.

โ‚ฟ Bitcoin is Dead! Quite a fun site, particularly at this time.

You’re not that good. Yet. ๐Ÿง˜โ€โ™‚๏ธ

๐Ÿ‘ This was a nice tweet storm about Balancer V2 and how Coinbase employees were going to invest in it. Not sure it came true but the V2 stuff is!

๐Ÿ“บ zkPorter seems like a novel tech and this explainer video was good.

๐Ÿ’ƒ Taylor Swift and NFTS, what’s not to like?

If working at Balancer ever gets non-fun then I imagine I’d like to do something like this. Also cool way to recruit. ๐Ÿค”

๐Ÿ‘ด๐Ÿผ How to Keep Your Edge as You Get Older – good podcast with ideas for me!

๐Ÿ”ฉ Etherscan & Tx Logs – some nice detail on how to walk these for info.

๐Ÿฆ Eth staking and Warren Buffet. It’s all about where you stand.

๐Ÿคฏ Uncovering a Four Year Old Bug. A glimpse into how samczsuns brain works hunting down a bug.

๐Ÿ”Œ Teardown of a PC power supply – always wanted to get a breakdown of a PSU and this is a really accessible one. Better than the majority of my Uni lectures!!

Photo by Ben White on Unsplash

SITIR 8

โ›“๏ธ Interesting Tweet from a project dev about writing his own code rather than use TheGraph. Covalent was mentioned as alternative – get events by topic hash or get events by contract address to fetch fully decoded log events for any contract.

๐Ÿ‘ Rules of thumb that simplify decisions

An explanation ๐Ÿค‘ about the different ways to make money with Options.

๐Ÿง™ Seems like good advice – 1/3 (networth) for the house, 1/6 to deal with expense / taxes, 1/2 rocket fuel.

โš’๏ธ Glitch – Build fast, full-stack web apps in your browser. Seems interesting and could be good for demos, docs or learning quickly.

Valentines isn’t far past so love โค๏ธ is in the air – 36 Questions That Lead To Love

โฑ๏ธ Time Billionaire – Is a pretty cool idea.

๐Ÿ” Pretty useful security tip – How to Ensure Youโ€™re Running the Legitimate Version of MetaMask

๐Ÿณ Wow – a wallet showing how the whales do it! ๐Ÿ’ต

Some more options craziness โ‚ฟ

More Burniske wisdom – it’s all waves man ๐Ÿ„โ€โ™‚๏ธ

๐Ÿคช Can’t get any crazier than 2020 – funny but not funny!

Getting In Good ๐Ÿƒ – Really interesting thread with examples of when to double down.

๐ŸŽง Bitcoin Vs Altcoins with Dan Held & Erik Voorhees – this was a great listen. Erik Voorhees is so articulate and I really like his logical and reasonable approach.

These Book Nooks are SO cool ๐Ÿค“

A case for the long BTC HODL ๐Ÿ’ช

โš ๏ธ Tail-end events are all that matter – a very tough lesson.

The Margin Loan – Interesting tradfi example of using leverage and the benefits of doing it ๐Ÿค”

And a DeFi version of above – FollowTheChain โ›“๏ธ & RicBurton demonstrating how using a MakerDAO vault to live of DAI and avoid selling your Eth during the Bear market.

Maker Vaults wouldn’t work with Liquidators ๐Ÿฆพ and this is a nice intro.

๐Ÿงฐ I’ve been working on Fleek and Cloudflare lately and it’s been fun – Fleek Makes Deploying and Maintaining an ENS+IPFS Website Easier than Ever

Photo by Antonio DiCaterina on Unsplash

SITIR 7

โ›ฝ GasTokens are pretty interesting and this was a good article.

๐Ÿ†’ Pretty cool (but expensive) screen project here.

Have a plan! Some good advice in this tweet. Particularly like – “But who really gives a shit about the “top” if you know exactly how much you need to retire and you get there well before the “top”? You just hit that number and you exit the game with a grin…Draw up your expenses… calculate how much you’ll have leftover after you pay taxes on some huge ass crypto gains. Then, assume about 5-7% a year in income thanks to some “safe” dividend stocks, maybe an annuity and some cefi stablecoin income…” ๐Ÿฆ‰

๐Ÿง˜ Also some good advice – On Despair and the Imagination.

โš ๏ธ Interesting tweet about BTC yield. Info about risks – > 50% flash crash risk >wBTC custodial risk > AAVE risks > Yearn/Curve risks, USDT risk too using 3pool. And how to maximise %, i.e. If using AAVE you borrow TUSD at 1.5% then swap to USDT. If using Compound you borrow DAI but receive COMP for overall around 0%.

๐ŸŽจ Some, cool, colourful and reasonably priced artwork.

๐Ÿ˜ข The Girl In The Window – wow, that was a hard hitting read.

๐ŸŽธ Long live Rock And Roll! Our โ€˜Lostโ€™ Weekend With Van Halen – A pretty amusing story that’s also a bit shocking in the way it shows how times have changed.

๐Ÿ–ผ๏ธ Some more cool art – this time Crypto Art (but a horrible website!)

๐Ÿ˜ฑ Big liquidation event on Compound because of DAI Oracle price. Pretty scary lesson learned here.

โ“ Good questions for the dinner table from Polina Marinovas newsletter – What was the most formative event in your life that you believe shaped who you are today? and What is one belief you hold that you would be afraid to share in public?

๐Ÿงฐ Recommended by rabmarut as an easier to use alternative to Metamask – Frame.

๐Ÿˆ Some of the Huddle Up posts are really good. “Equity Deals Only” about DeAndre Hopkins was worth a read.

Photo by Kyle Glenn on Unsplash

Etherscan API

Etherscan is a really useful website but fairly recently I discovered they also have a pretty handy Developer API. I used it to analyse gas a contract was using over a period of time and thought it might be useful to record.

Getting Started

First of all an API key is required and this can be created for free by logging in and going to: https://etherscan.io/myapikey.

I’m going to paste my code with comments which should be easy enough to follow. The API covers a whole lot of other end point such as Logs, Tokens, etc that I didn’t use but the docs are good and worth a scan.

Example

Photo by Markus Spiske on Unsplash

SITIR 6

๐Ÿค Eth Network Discord Community – I liked the idea about this from DeFi Dudes tweet.

๐Ÿฅง I’ve used RaspberryPi a lot and the new Pi 400 seems cool.

๐ŸŽง Uncommon Core seems like a good podcast to add to the list and this was an interesting thread discussing some of points made in the last episode.

๐Ÿง‘โ€๐Ÿ’ป Then new Ethereum.org dev portal was released and is probably a go to resource now.

๐Ÿฅช Mmmmmhhhh sandwiches!

๐Ÿ‘ฉโ€๐Ÿซ 40 things I learned by age 40 by Sunday Soother has some really good advice worth re-reading every now and then.

I don’t really know much about yield curves and things but fixed rate lending/borrowing on Ethereum would be ๐Ÿ‘Œ

And along similar lines this Rise of the Cryptodollar article from Bankless discusses savings rates for crypto dollars.

๐ŸŒ๏ธโ€โ™‚๏ธ Didn’t realise half this stuff about the Masters finances and thought it was pretty interesting.

๐Ÿง˜ This so true:

Working on a problem reduces the fear of it.

Itโ€™s hard to fear a problem when you are making progress on itโ€”even if progress is imperfect and slow.

Action relieves anxiety.

James Clear 3-2-1