art with code

2021-11-24

Azure Files with a high number of Hot Writes

My Azure Files bill for a small 80 GB project share was kinda high. Around $30 per month. But the storage cost should be under $5. What's going on in here?

Azure's non-Premium storage tiers bill you for transactions (basically filesystem syscalls.) I was seeing 450 Create calls per a 5 minute period in Monitoring/Insights. On the Hot storage tier, that amounts to an extra $25 per month. But the load went away during night time. Around the time when I put my laptop to sleep.

Clearly the laptop was doing something. But what... Right. I had an open VSCode window for editing a project in a folder on the fileshare. Running a serve in a terminal too. Closed the VSCode window and the transactions per 5 minutes went to 0. That's a $25/month VSCode window. I guess it counts as SaaS.

Moral of the story? Billing for syscalls is a good business model.

2021-11-02

BatWerk - One Year In

The BatWerk exercise app (Android iOS) keeps you healthy, happy and productive with minimal effort.

This series talks about the different aspects of the app and how I'm approaching them in BatWerk (IntroMaintaining the routineHow to play).

Interested? Give it a try.

One Year In

It's now been a year since the Halloween party app started taking a life of its own and turned into a wellness app, keeping the bats along for the ride (and pumpkins too, for the first six months.) I've used it personally throughout the year and seen it evolve from a HIIT-centric workout app into an ambient couple-minutes-at-a-time exercise app, into a more complete mood improver and planned activity driver. There are still ways to go, so enjoy the ride.

Over the year, I've collected 105k coins, which translates to roughly 170 hours of exercise. That's half an hour of exercise a day, not bad for a bunch of bats!

And with the ring game distributing the effort over the day, it's been possible to do it even when the outside temps have been +37C. It's also kept many pains at bay by getting me moving before the mildly discomfortable sitting position turns into a painful locked up neck.

The weird thing is that I don't feel like I've done much exercise. In the good and the bad: the mood boost isn't as good as with a 15-min workout, or an one hour run, but the minutes rack up and you don't get the muscle pains and tiredness. I'd like to mix it up with workouts or runs or something, just for the extra mood boost.

Onwards to year two!

Where to next?

To improve the lives of as many people as possible, the BatWerk movement needs more people doing it and improvement in the quality of activities we're doing, exercise and otherwise.

This is the guiding equation: impact = userCount * userMinutesPerDay * qualityOfLifeImprovement(userMinutesPerDay)

My definition for the quality of life improvement is feeling healthy, not in pain, having a positive mental state, maintaining good relationships, getting my day-to-day tasks done, and achieving my long term goals.

To reach these ambitious goals, we need people who can get people doing the BatWerk routine. We need people who can increase the routine's quality of life improvement, and help users reach their optimal minutes per day. We need people who can get other people execute at the top of their game. Finally, we need people who can keep everyone paid and enable them to work on BatWerk in their fullest capacity.

Let's do it! Drop me a mail.

Current state of werk

Currently, the quality of life improvement from BatWerk is good on exercise minutes (let's say 80% on target with 30 min/day - I'd like to add some occasional intensity), mediocre on mood improvement (30% - the movement and messaging help, but it doesn't create a very solid mood), poor on driving goal-achieving action (20% - it drives initial action, but tends to lead to diluted effort and low-impact actions.) The challenge is habit-stacking these in a mutually reinforcing manner. If you add a badly-designed painful action driver after the exercise ring, it not only makes the action unlikely to happen, but makes you want to avoid doing the exercise as well.

In terms of user minutes per day, BatWerk is pretty much on target. There are ways to bring it up in a way that improves the quality of life metric, but it's good as it stands.

At this time, the most impactful way to increase BatWerk impact is increasing the user count by introducing more people to the activity and improving initial user retention. That will likely require a different packaging for the activity to make it a better identity fit, alongside a great effort to tell more people about the activity (and improving the design in a direction that makes people tell their friends about it too.)

Sustainability of the BatWerk movement is precarious. I'm working on it on my spare time, it has zero revenue, and based on my previous experience with monetizing websites and apps the revenue is unlikely to exceed $20 per year. That's twenty dollars, yes. To fix this dangerous situation, the priorities are to find a person who can create enough cashflow to sustain their work on BatWerk impact, and to increase the cashflow as a function of the impact to increase the size of a potential team. With the cashflow more secure and scalable, the priority shifts to hiring people who can increase the BatWerk impact, and building and improving the impact-generation machine.

2021-09-27

Quickgres

Quickgres is a native-JS PostgreSQL client library.

It's around 400 lines of code, with no external dependencies.

One afternoon a few years ago I thought that it'd be fun to write a web framework for SPAs in node.js. The result of that effort is qframe. Qframe is a post for another time, but its design philosophy led me down the rabbit hole of writing a PostgreSQL client library. See, qframe was minimalistic. The whole framework fits in 300 lines of (mildly over-dense) code (read it, it's highly commented.) And the `pg` client library was kinda big. So. Hey. Writing a Postgres client library can't be much harder than a web framework, right?

400 lines of code later: it was a little bit harder (33% harder if you go by lines of code.) But man, the power. If you control the server, the database layer and the client, you can do some crazy stuff. For example, streaming database responses directly to the client socket and having the client parse the Postgres protocol (see the `DB.queryTo` bits in the quickgres branch of qframe.) This can make the web server super lightweight, many HTTP request handlers become "write SQL stored procedure call to DB socket, memcpy the response to the HTTP socket."

Quickgres is a pipelined PostgreSQL client library. The core loop writes queries to the connection socket as they come. Once the responses arrive they are copied off the receive buffer and passed to the query promise resolvers. You can have thousands of queries in-flight at the same time. This gives you high throughput, even over a single database connection. The connection buffer creation and reading is optimized to a good degree. Responses are stored as buffers, and only parsed into JS objects when needed (which ties to the above stream-straight-to-client example, the server can get away with doing minimal work.)

There's no type parsing - it was difficult to fit into 400 lines of code. Type parsing is also subtle, both in terms of accurate type conversions and performance implications. In Quickgres you have to explicitly cast your JS objects into strings or buffers to pass them to the DB layer. You'll know exactly what the database receives and how much work went into producing the string representation.

As I mentioned above, Quickgres is around 400 lines of code. Have a read, it's commented (though not quite as well as Qframe). The core bits are Client onData and processPacket. The onData function parses the next packet out of the connection socket, and passes it to processPacket. Most of the rest of the Client is a bunch of functions to create and fill buffers for different PostgreSQL protocol messages. The RowReader and RowParser classes deal with parsing query results out of received packets. Reading it now, I might not want to make a magical struct (i.e. access columns through `row.my_column`) in the RowParser and instead have `.get('my_column')` API for simplicity. Anyway, the generated RowParser structs are reused by all invocations of the stored procedure, so it shouldn't be a major performance issue. You can also get the rows as arrays, if needed.

Performance-wise, I was able to read a million rows per second on a 2018 MacBook Pro over a single connection. Queries per second, around 45k/connection. With multiple cores, you can get anything from 130k-750k SELECT queries per second. For SELECT followed by UPDATE, my best results were 100k/sec. You may be able to eke a few billion queries per day out of it if your workload and server agree.

I tried out connection pooling, but it doesn't give you more performance, so there's no built-in pooling mechanism. You could use a connection pool to smooth out average response times if you have a few queries that take a long time and everything else running quick (but in that case, maybe have just two connections: one for the slow queries and the other for everything else.) The main reason to "pool for performance" is if your client library doesn't pipeline requests. That will add the connection latency to every single query you run. Let's say your DB can handle 30k queries per second on localhost. If you have a non-pipelined client library that waits for a query to return its results before sending out the next one, and you access a database with 20ms ping time, you'll be limited to 50 queries per second per connection. With a non-pipelined client each query needs to be sent to the database, processed, and sent back before the next query can be executed. With a pipelined client, you can send all your queries without waiting, and receive them back in a continuous stream. You'd still have minimum 20 ms query latency, but the throughput is no longer latency-limited. If you have enough bandwidth, you can hit the same 30kqps as on localhost.

It has tests. But they require some test table creation beforehand. Which I never got around to scripting. (In case you want to run the tests: `CREATE TABLE users (id uuid, name text, email text, password text)`, fill with a million users, with numbers from 0 to 999 999 as emails, and one user with id 'adb42e46-d1bc-4b64-88f4-3e754ab52e81'.)

If you find this useful or amusing, you can send us a million dollars (or more!) from your cloud budget every year to support our great neverending work. Because it is Great and Neverending. It literally Never Ends. How much is a paltry million in comparison to the Infiniteness of Space? Nothing. Less than nothing. So send it now. Send, then, as much as your father did! Send to ETH address 0x24f0e742f5172C607BC3d3365AeF1dAEA16705dc

The proceeds will be spent on BatWerk to make you healthier and happier.

README

Features

  • Queries with parameters (along with prepared statements and portals).
  • Each parameterized query creates a cached prepared statement and row parser.
  • COPY protocol for speedy table dumps and inserts.
  • Lightly tested SSL connection support.
  • Plaintext & MD5 password authentication.
  • Partial query readback.
  • You should be able to execute 2GB size queries (If you want to store movies in TOAST columns? (Maybe use large objects instead.)) I haven't tried it though.
  • Canceling long running queries.
  • Binary params, binary query results.
  • Fast raw protocol pass-through to output stream
  • Client-side library for parsing PostgreSQL query results in the browser

Lacking

  • Full test suite
  • SASL authentication
  • Streaming replication (For your JavaScript DB synced via WAL shipping?)
  • No type parsing (This is more like a feature.)
  • Simple queries are deprecated in favor of parameterized queries.

What's it good for?

  • It's relatively small so you can read it.
  • It doesn't have deps, so you don't need to worry about npm dephell.
  • Performance-wise it's ok. Think 100,000 DB-hitting HTTP/2 requests per second on a 16-core server.

Usage


const { Client } = require('quickgres'); 

async function go() {
    const client = new Client({ user: 'myuser', database: 'mydb', password: 'mypass' });
    await client.connect('/tmp/.s.PGSQL.5432'); // Connect to a UNIX socket.
    // await client.connect(5432, 'localhost'); // Connect to a TCP socket.
    // await client.connect(5432, 'localhost', {}); // Connect to a TCP socket with SSL config (see tls.connect).
    console.error(client.serverParameters);

    // Access row fields as object properties.
    let { rows, rowCount } = await client.query(
        'SELECT name, email FROM users WHERE id = $1', ['adb42e46-d1bc-4b64-88f4-3e754ab52e81']);
    console.log(rows[0].name, rows[0].email, rowCount);
    console.log(rows[0][0], rows[0][1], rowCount);

    // You can also convert the row into an object or an array.
    assert(rows[0].toObject().name === rows[0].toArray()[0]);

    // Stream raw query results protocol to stdout (why waste cycles on parsing data...)
    await client.query(
        'SELECT name, email FROM users WHERE id = $1', 
        ['adb42e46-d1bc-4b64-88f4-3e754ab52e81'], 
        Client.STRING, // Or Client.BINARY. Controls the format of data that PostgreSQL sends you.
        true, // Cache the parsed query (default is true. If you use the query text only once, set this to false.)
        process.stdout // The result stream. Client calls stream.write(buffer) on this. See RowReader for details.
    );

    // Binary data
    const buf = Buffer.from([0,1,2,3,4,5,255,254,253,252,251,0]);
    const result = await client.query('SELECT $1::bytea', [buf], Client.BINARY, false);
    assert(buf.toString('hex') === result.rows[0][0].toString('hex'), "bytea roundtrip failed");

    // Query execution happens in a pipelined fashion, so when you do a million 
    // random SELECTs, they get sent to the server right away, and the server
    // replies are streamed back to you.
    const promises = [];
    for (let i = 0; i   1000000; i++) {
        const id = Math.floor(Math.random()*1000000).toString();
        promises.push(client.query('SELECT * FROM users WHERE id = $1', [id]));
    }
    const results = await Promise.all(promises);

    // Partial query results
    client.startQuery('SELECT * FROM users', []);
    while (client.inQuery) {
        const resultChunk = await client.getResults(100);
        // To stop receiving chunks, send a sync.
        if (resultChunk.rows.length > 1) {
            await client.sync();
            break;
        }
    }

    // Copy data
    // Let's get the users table into copyResult.
    const copyResult = await client.query('COPY users TO STDOUT (FORMAT binary)');
    console.log(copyResult.rows[0]);

    // Let's make a copy of the users table using the copyResult rows.
    const copyIn = await client.query('COPY users_copy FROM STDIN (FORMAT binary)');
    console.log(copyIn.columnFormats);
    copyResult.rows.forEach(row => client.copyData(row));
    await client.copyDone();

    await client.end(); // Close the connection socket.
}

go();

Test output

On a 13" Macbook Pro 2018 (2.3 GHz Intel Core i5), PostgreSQL 11.3.


$ node test/test.js testdb
46656.29860031104 'single-row-hitting queries per second'
268059 268059 1
268059 268059 1

README tests done

received 1000016 rows
573403.6697247706 'partial query (100 rows per execute) rows per second'
received 10000 rows
454545.45454545453 'partial query (early exit) rows per second'
warming up 30000 / 30000     
38510.91142490372 'random queries per second'
670241.2868632708 '100-row query rows per second'
925069.3802035153 'streamed 100-row query rows per second'
3.0024 'stream writes per query'
1170973.0679156908 'binary query rows per second piped to test.dat'
916600.3666361136 'string query rows per second piped to test_str.dat'
595247.619047619 'query rows per second'
359717.9856115108 'query rows as arrays per second' 10000160
346505.8905058905 'query rows as objects per second' 1000016
808420.3718674212 'binary query rows per second'
558980.4359977641 'binary query rows as arrays per second' 10000160
426264.27962489345 'binary query rows as objects per second' 1000016
Cancel test: PostgreSQL Error: 83 ERROR VERROR C57014 Mcanceling statement due to user request Fpostgres.c L3070 RProcessInterrupts  
Elapsed: 18 ms
Deleted 1000016 rows from users_copy
47021.94357366771 'binary inserts per second'
530794.0552016986 'text copyTo rows per second'
461474.8500230734 'csv copyTo rows per second'
693974.3233865371 'binary copyTo rows per second'
Deleted 30000 rows from users_copy
328089.56692913384 'binary copyFrom rows per second'

done

Testing SSL connection
30959.752321981425 'single-row-hitting queries per second'
268059 268059 1
268059 268059 1

README tests done

received 1000016 rows
454346.2062698773 'partial query (100 rows per execute) rows per second'
received 10000 rows
454545.45454545453 'partial query (early exit) rows per second'
warming up 30000 / 30000     
23094.688221709006 'random queries per second'
577034.0450086555 '100-row query rows per second'
745156.4828614009 'streamed 100-row query rows per second'
3 'stream writes per query'
1019379.2048929663 'binary query rows per second piped to test.dat'
605333.5351089588 'string query rows per second piped to test_str.dat'
508655.13733468973 'query rows per second'
277243.13834211254 'query rows as arrays per second' 10000160
252848.54614412136 'query rows as objects per second' 1000016
722033.21299639 'binary query rows per second'
432907.3593073593 'binary query rows as arrays per second' 10000160
393242.62681871804 'binary query rows as objects per second' 1000016
Cancel test: PostgreSQL Error: 83 ERROR VERROR C57014 Mcanceling statement due to user request Fpostgres.c L3070 RProcessInterrupts  
Elapsed: 41 ms
Deleted 1000016 rows from users_copy
33407.57238307349 'binary inserts per second'
528829.1909042834 'text copyTo rows per second'
501010.0200400802 'csv copyTo rows per second'
801295.6730769231 'binary copyTo rows per second'
Deleted 30000 rows from users_copy
222176.62741612975 'binary copyFrom rows per second'

done

Simple simulated web workloads

Simulating web session workload: Request comes in with a session id, use it to fetch user id and user data string. Update user with a modified version of the data string.

The `max-r` one is just fetching a full a session row based on session id, so it's a pure read workload.


$ node test/test-max-rw.js testdb
    32574 session RWs per second              
done

$ node test/test-max-r.js testdb
    130484 session Rs per second              
done

Yes, the laptop hits Planetary-1: one request per day per person on the planet. On the RW-side, it could serve 2.8 billion requests per day. Note that the test DB fits in RAM, so if you actually wanted to store 1k of data per person, you'd need 10 TB of RAM to hit this performance with 10 billion people.

On a 16-core server, 2xE5-2650v2, 64 GB ECC DDR3 and Optane. (NB the `numCPUs` and connections per CPU have been tuned.)


$ node test/test-max-rw.js testdb
    82215 session RWs per second              
done

$ node test/test-max-r.js testdb
    308969 session Rs per second              
done

On a 16-core workstation, TR 2950X, 32 GB ECC DDR4 and flash SSD.


$ node test/test-max-rw.js testdb
    64717 session RWs per second              
done

$ node test/test-max-r.js testdb
    750755 session Rs per second              
done

Running server on the Optane 16-core machine, doing requests over the network from the other 16-core machine.


$ node test/test-max-rw.js testdb
    101201 session RWs per second              
done

$ node test/test-max-r.js testdb
    496499 session Rs per second               
done

2021-09-03

BatWerk 5 - How to play

The goal of the BatWerk exercise app (Android iOS) is to keep you healthy, happy and productive without requiring you to overhaul your life. This is a series of blog posts that talks about the different aspects of an exercise app and how we're approaching them in BatWerk (IntroHow do muscles workPainsMaintaining the routineHow to play). Interested? How would you like to improve it?

How to play?

There are two ways to play BatWerk: The hand-held mode and the free space mode. In the hand-held mode you hold the phone in your hand and tilt the phone or move your head to move your character as you do the moves. In the free space mode, you place the phone on a chair and move in the camera to move the character and do the moves.

For example, you get the move "Look left and right" with sports balls appearing on the left and right sides of the screen. In the hand-held mode, you'd hold the phone in front of your face and look to your left to pick up the first ball, then look to your right to pick up the next ball. Your hand holding the phone would stay stable in front of you, just your head would move and exercise your neck.

In the free space mode, you'd place the phone on a chair or low table in front of you so that your entire body is in the camera view. To pick up the balls, you'd move your body to look left and then to look right, like a dancer. You could also pick up the balls by stepping from left to right. The exact way you do it doesn't matter so much, as long as you move in a good form and don't hurt yourself.

If you don't know how to do the moves, look at what the guide character is doing and copy that. If you're having difficulty moving the character, check the camera view for image quality. You should have your head in view reasonably lit (i.e. not totally dark or super backlit). The free space mode works best if you keep your entire body visible in the camera frame.

Most of the moves are on timers, the reps are up to you. Some moves have a fixed number of reps, but the pace is up to you. If these moves are too much work, you can go to the phone and tap through them. If you can't or don't want to do a particular move, you can do something else instead or press the skip button. The goal is to move for a couple minutes, and the suggested moves are just suggestions that you don't have to follow.

As you do moves, you earn coins and complete rings. A minute or two of moving is enough to fill up one of the small rings. There are 12 small rings in total every day. A new ring starts filling up every half hour, so the way you play is to do a couple minutes of moves, fill up the ring, then go back to doing other things for half an hour or hour, and come back to do the next ring. Complete all the small rings to get half an hour of exercise spread across the day. Every day at midnight, the rings reset and you start anew.

The game design might sound a bit odd, but there are scientific reasons behind it. You should move for about two minutes every half an hour according to research. But life doesn't always allow you to take a few minutes to power up. Or maybe you're on a long walk already, so extra moves on top of that would be pointless. Whatever the case, you only need to do moves across six hours of the day.

Workouts

The workout mode takes you through a 15-minute random workout. Put on music and move to the beat and it's good fun. The first set is a gentle warmup, followed by the four main sets, and the stretching set at the very end. I don't do the workout very often (+32C summer, ugh), but it's a great mood lifter. Fist pumps and cheers all around.

If a move in the workout is too tough, or it's a floor move and I'm outside, I do something else. Burpees become crouching jumps, sit-ups turn into bending backwards, and so on. Most moves don't have a number of reps, just a timer. And if there's a fixed number of reps, you can tap through it after you get fed up.

As you might guess, it's not exactly a Spartan routine where you curse the app and the developer after failing to complete the first half of the "Beginner Workout". At least I hope so. Have fun, don't overdo it. The workout mode is there for having fun and getting your mood up.



2021-09-02

BatWerk 4 - Maintaining the routine

The goal of the BatWerk exercise app (Android iOS) is to keep you healthy, happy and productive without requiring you to overhaul your life. This is a series of blog posts that talks about the different aspects of an exercise app and how we're approaching them in BatWerk (IntroHow do muscles workPains, Maintaining the routine, How to play). Interested? How would you like to improve it?

The Hardest Part

The hardest part in any exercise routine is keeping it up. Exercise is an unnatural waste of energy that you do maintain yourself. Your brain really really doesn't want to do exercise. In the olden days, you had to walk 4 hours every day to find enough food to eat. Any extra movement outside of that was going to burn off your energy reserves and require a longer walk the next day.

Sure, it may be fun to exercise, but it tends to be a very optional part of your life. It's not like eating, breathing or sleeping. Or talking for that matter. You can easily take a year off exercise. I did that. I used to do a daily 15-minute exercise routine for two years, but then we had our first child and I just gradually stopped doing the routine. For years. I only really started getting back on track when I started developing BatWerk. In that sense, it's been a personal success already. Now I want to help more people enjoy the benefits that come from using BatWerk. (And, well, if I want to keep on using it, it needs to make enough money to fund a team that keeps developing it.)

The reason you can stop exercising in the first place is because your activity driving system doesn't see exercise as necessary. From its hunter-gatherer perspective, you're going to get exercise anyway in your hunt for the necessities, so there's no need to drive extra movement - if anything, the activity driver wants you to use as little energy as possible.

The problem is that many systems in your body take the aforementioned 4 hours of movement as granted. I mean, to eat, you're going to need to walk 4 hours in rough terrain anyhow, right? Otherwise you'd starve and die, right? So there's no point in assuming you won't get 4 hours of walking every day, right? 

Enter modern life with its ridiculous amounts of easily available food. Couple that with the hunter-gatherer activity driver that responds to available food by resting and feasting. It was rare to find loads of good food, so the best course of action was to load up before it ran out or the competition showed up.

If you have lots of food available, you don't move much. You likely have lots of food available right now. So you don't move much. And the systems in your body that rely on movement don't work so well. 

For example, your veins rely on muscle contractions to transport blood back to your heart, your lymphatic system uses your movements to clear out waste from your tissues, your guts use the walking motion to help with gut mass transport, your joints are lubricated by movement, your bones, muscles and connective tissues grow and strengthen in response to usage. If you don't move enough, you start having health issues. And your activity driver reacts to health issues by asking you to rest, leading to more issues.

That's the problem. Exercise is necessary for your body, but it's linked to thirst, hunger and threat avoidance in your brain. Remove thirst, hunger and threats and your brain switches your body to power-saving mode. Your body doesn't work properly if it's in power-saving mode for long periods of time. What to do?

Activity Driver

The key to an exercise app that makes you actually exercise is to create an activity driver. Much like the habit-creating loops in social media apps and games, an exercise app needs to wrap the unnecessary exercise activity in a shell that drives continuous activity. It needs to make optional and unnecessary exercise into required and necessary exercise.

What we have in BatWerk is a system to drive regular exercise. The frictionless design removes barriers to getting started. The ring game drives movement throughout the day. The reward system keeps you coming back. The messaging creates an identity around taking responsibility, doing, and finishing.

The design of the activity is meant to drive daily movement because that's what you need to avoid the damage from not moving - exercise needs to be regular, like eating. A large part of the design is about getting you started every day. Once you start, the ring game pulls you towards completing all rings that day.

The short-term rewards have a random element, which drives you to continue the move loop. The long-term accumulation of rewards creates a sunk cost and a status symbol, which makes you place a higher value on the exercise you've done. The incomplete pattern in the ring game makes you want to complete the pattern by collecting the rings. The time limits in the ring game drive action in the now.

Taken together, you've got a system that gets you do the first move, uses that to drive you to do a couple minutes of moves, uses that to make you want to do 30 minutes of moves over the day, and uses the rewards from the daily moves to make you want to come back tomorrow. It's a tricky thing to build, and I could use help to get it smoother and more engaging. 

2021-08-25

BatWerk 3 - Pains

The goal of the BatWerk exercise app (Android iOS) is to keep you healthy, happy and productive without requiring you to overhaul your life. This is a series of blog posts that talks about the different aspects of an exercise app and how we're approaching them in BatWerk (IntroHow do muscles workPainsMaintaining the routineHow to play). Interested? How would you like to improve it?

Where does the pain come from? You know. The one that you get from wearing headphones all day. Or the one from sitting at your desk tapping away at the secrets of the universe. Or that one where you're thumbing your mobile for hours on end.

Pain

The initial thoughts I had around pain: Pain is a bunch of nerves firing pain signals and those getting carried up to the brain. Painkillers block the inflammation response without fixing the root cause. The nerves fire pain signals because they're activated in some way. Maybe there's something pressing on the nerve, maybe the nerve is under tension, maybe there's damage of some sort that's causing the nerve to fire. 

Going from there, what could be pressing on the nerve? Holding your body in a painful position. Build-up of liquid that increases pressure on the nerve. Inflammation. Damage to the nerve.

Some causes are difficult to fix by yourself, but some are more feasible. Painful position? Change position. Find a good sleeping position. Static load on muscle that causes build-up of liquid (e.g. using a computer / phone for a few hours)? Take regular breaks, try to unlock the muscle by locating it with status query meditation, massage, and 10-15 minutes of varied moves. Throw in a painkiller to help reduce the inflammation. Wearing headphones and getting neck pain? Try earbuds. Pain on the sides of your head? Bring your display down so that you don't have to look up so much. 

Wrists hurting after a day at the keyboard? Wear mittens. Set a timer to lock the screen every 15 minutes. Do pushups, curls and grips to strengthen your wrists and fingers. Move your fingers less when typing. Use a flat keyboard with minimal force required.

Prevention design

Doing the micro-exercises every half an hour over the work day has been a pretty good preventive measure. It makes me move often enough and with enough variety that I don't get as many static load lockups. Still happens sometimes though, especially when I zone into something for more than an hour, or keep working in a weird position for a while. The workout and the neck stretch set can help there but it's kind of tough to get yourself through 15 minutes of movement when your head is splitting and you just want to sleep.

What sometimes helps is the painkiller, movement and massage -combo. Block the pain to loosen up the muscle, then wash the dishes to shake them more, find out which neck muscle is hurting and massage that. It usually gets me far enough that I can fall asleep and rely on the sleep relaxation to fix stuff up.

The BatWerk moves contain a bunch of neck motions and core moves, twists and stretches because a major goal for the app is to act as a preventive painkiller. If your body doesn't lock up, it's not going to get lock up pains, right? Would love to make it even better, so let me know if you can help.

Deep pain

What we have above are high level features of sedentary office worker pain. Folk medicine. But what is actually happening on a deeper level when your wrist starts to hurt?

Okay, so, let's say that you have the initial twinge of pain from the median nerve inside your wrist getting squeezed. First you get the fast pain, which is followed a bit later by the dull pain. Fast pain is a signal from a pain receptor that travels along an A nerve fiber. Dull pain is a pain signal that travels on a C nerve fiber.

The difference in the speed of sharp pain and dull pain comes from the different nerve fiber types. The A fibers are myelinated - they're wrapped in an insulating sheath that helps nerve signals travel faster. The C fibers are smaller and don't have the myelin wrapper, so it takes a few seconds for the signal to reach the brain.

As the nerve signal leaves the pain site at the wrist, it travels up the median nerve to the brachial plexus in the shoulder, where it splits into the medial cord and the lateral cord. The cords then further split and merge into the five root nerves connected to the spinal segments.

The pain signal enters the spinal cord at the dorsal horn, which can change the intensity of the signal. If you're pre-occupied with something, the dorsal horn can block the pain from reaching your brain. Then when you stop doing whatever you were doing, you start feeling the pain.

From the spinal cord, the pain signal travels via the spinothalamic tract to the brain. And now you're in pain.

Sensors

Okay. Now we know the road pain travels. But what is it exactly?

Our sensory neurons detect changes in their neighborhood. There are sensors all around your body, for example on the skin and eyes, in your muscles, joints, organs and the digestive tract. These sensors are not only for pain, but are also responsible for sensory perception and your body sense (i.e. the position of your limbs relative to each other).

Different sensor neurons detect pressure changes, stretching, presence of chemicals, and temperature changes. The pain sensors are tuned to fire only when the stimuli exceeds a specific threshold. The threshold isn't fixed, and can change as a result to changes in the environment. For example, a gentle touch can be quite painful on an inflamed area.

As the sensor is stimulated, it builds up an electric potential. As it exceeds the threshold value, it sets off an action potential (a kind of wave of flipped polarity) down the nerve. Electric? Yeah, all your cells maintain -70mV voltage between the inside and outside of the cell. Messing with this voltage is what neurons use for internal messaging.

A neuron consists of a receiving end and transmitting end. The two ends of a neuron are connected by a cable called the axon. The receiving end sets off the action potential to tell the transmitting end to release neurotransmitter molecules. These are picked up by the receiving ends of the neurons next in the chain (which makes them more or less likely fire off their own action potentials). There are hundreds of different neurotransmitters, but each neuron usually releases only one kind. Pain sensors use glutamate? Noradrenaline?

Putting it all together, tissue damage causes immune cells to release cytokines and prostaglandins that create inflammation which releases histamines and other molecules that are picked up by nociceptors, making them more sensitive and turning the inflamed area painful to touch. The inflammation also leads to the production of lysophosphatidic acid and sphingosine-1-phosphate that directly activate TRPV1 high temperature nociceptors responsible for burning sensations, causing a voltage potential buildup that exceeds the nociceptor's threshold, making it fire an action potential to its A fiber and C fiber axon ends, where the change in voltage triggers the intake of calcium ions into the cell, which signals the axon to bring neurotransmitter vesicles to pores in the cell membrane and release the contained glutamate molecules into the synaptic cleft, where the dendrite of the next neuron picks them up and excites the neuron, making it propagate the signal. On reaching the spinal column, the dorsal horn attenuates the pain signal with GABA and noradrenaline neurotransmitters before forwarding it on along the spinothalamic tract to the brainstem and thalamus. And now you're feeling burning pain at the inflammation site and it's become sensitive to touch.

This feels like one of those "what happens when you click on a link"-explainers that ends up with an explanation of how to build an adder circuit out of transistors. Anyway, that was an educational journey.

Nociceptor Sensory Neuron-Immune Interactions in Pain and Inflammation https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5205568/

General Pathways of Pain Sensation and the Major Neurotransmitters Involved in Pain Regulation https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6121522/

Presynaptic Inhibition of Pain and Touch in the Spinal Cord: From Receptors to Circuits [PDF] https://www.mdpi.com/1422-0067/22/1/414/pdf


2021-08-23

BatWerk 2 - On muscles

This is a series of blog posts that talks about the different aspects of an exercise app and how we're approaching them in BatWerk (IntroHow do muscles workPainsMaintaining the routineHow to play). Interested? How would you like to improve it?

The goal of the BatWerk exercise app (Android iOS) is to keep you healthy, happy and productive without requiring you to overhaul your entire life.

So, of course I ended up wandering down some researchy rabbit holes to figure out how to do that. One of these was figuring out what actually happens when you do exercise. Why does your body move in the first place and how does it go about achieving that. Here are some confused notes on the whole process. Comments and corrections welcome, I'm out of my depth here!

What's a Muscle Anyway

Hey, your muscles are bags of string-like proteins that slide across each other with the help of ATP molecules. The proteins start moving and using ATP after calcium ions released by motor nerve activation open a lock molecule. Each of the sliding proteins generates force on the piconewton scale. To lift your arm, trillions of proteins need to slide in unison.

The three ways to make your muscles work better are to increase the number of muscle proteins, increase the amount of ATP available, and improve the motor nerve firing.

Moving a muscle activates it and makes it grab oxygen, sugars and fat from your bloodstream for ATP generation. The more capillaries you've got around the muscle, the more ATP it has available. Activating muscles also drives growth hormone secretion, which makes the muscles grab proteins from the bloodstream for conversion into muscle protein, and stimulates the growth of capillaries. Conversely, inactive muscles go into power saving mode where they don't pull in fuel and proteins, leading to lower metabolic rate and loss of muscle proteins.

Activating muscles to perform motions also trains your motor nerves to fire at the right time. Motor nerves activate only a part of the available muscle fibriles to contract, cycling through these activation groups over the contraction period. Motor nerves need to be trained through repeated use. Moves also require technique training to accomplish them through muscle contractions.

The ATP used by the proteins comes in four ways. First is the existing reserves inside the muscle. These are used up in about 3 seconds. Then come the muscle's creatine phosphate reserves. These are converted to ATP and last for a bit more than 5 seconds. Next up is anaerobic conversion of sugars to ATP, which can be maintained for a minute or two. After that, the muscle switches to aerobic ATP conversion from sugars if available, falling back to fats and even proteins if not.

Anaerobics

The thing about anaerobic and aerobic conversion is that aerobic conversion of sugars is around 15 times more efficient than anaerobic (30 aerobic ATP per glucose molecule vs 2 anaerobic ATP per glucose molecule). If you operate aerobically, you'll have to do 15 times the work to process the same amount of sugar. In other words, a 800m sprint needs to process as much sugar as a 10k run.

Sounds wasteful, right? But your body has a trick up its sleeve, called the Cori cycle. The anaerobic splitting of glucose also yields two molecules of pyruvate that get fermented to lactate. In the Cori cycle, the lactate is taken to the liver and converted back to glucose. However, this conversion uses up  6 ATP molecules, so it's not a perpetual motion sugar-recycling machine. In aerobic ATP generation, the pyruvate is taken through the Krebs cycle in mitochondria instead.

So, while on the whole, the Cori cycle loses 4 ATP, the liver's ATP molecules can come from aerobic generation of ATP before or after the anaerobic exercise. If you go through the Cori cycle once before switching to aerobic generation of ATP, you'll generate 26 ATP with the Cori cycle vs 30 ATP when fully aerobic. Less efficient, sure, but not 2 vs 30.

This creation of glucose from other molecules is called gluconeogenesis and it's also how your body maintains your blood sugar levels during intense exercise, starvation or a low-carb diet. In a way, your body is a factory dedicated to converting stuff to glucose, storing it, transporting it, and creating ATP out of it. Which makes sense on a cellular level, cells are all about breaking down complex molecules for energy and using the energy to build other complex molecules.

ATP ADP GTP GDP NAD NADH FAD FADH2 WHAT

ATP molecules aren't so much created from sugar, as they are "recharged". When a muscle protein uses ATP to do its tiny sliding walk, the ATP is converted into ADP by breaking off one phosphate group. This ADP then gets turned back into ATP by attaching the phosphate group back to it.

ADP stands for adenosine diphosphate, and ATP stands for adenosine triphosphate, so you can see how the addition of a phosphate turns a diphosphate into a triphosphate and vice versa. And yes, there's also AMP, or adenosine monophosphate.

Similarly, the NAD+ and FAD+ ions (nicotinamide adenine dinucleotide and flavin adenine dinucleotide, respectively. Those adenines keep popping up everywhere, huh?) are recharged to NADH and FADH2 molecules in the Krebs cycle.

The Krebs cycle can generate either ATP or GTP (guanosine triphosphate). GTP can be converted to ATP by swapping the phosphate chain of an ADP for the GTP's one. This is done by a molecular machine called the nucleoside-diphosphate kinase that turns the ADP into an ATP and the GTP into a GDP (guanosine diphosphate). This swap can be done in the other direction as well. Muscle cells tend to generate ATP in the Krebs cycle as they use a lot of ATP. Other tissues like the liver can generate more GTP, as it's useful in protein synthesis.[Citric Acid Cycle]

Right. To wrap up, your muscle cells break down sugar to convert ADP to ATP by adding one phosphate group to it. The ATP is used by the proteins in your muscles to help them climb past each other, which breaks off the phosphate group and creates ADP. The sugar can be processed anaerobically or aerobically. The aerobic processing is an extra phase added after the anaerobic process, and can be delayed for a later date with the help of lactic acid.

This sugar processing cycle is happening right now inside each of the 37 trillion cells of your body. Every move you make requires coordinating trillions of proteins to react in the right way at the right time.


2021-08-16

BatWerk - Intro

The goal of the BatWerk exercise app (Android iOS) is to keep you healthy, happy and productive without requiring you to overhaul your life. This is a series of blog posts that talks about the different aspects of an exercise app and how we're approaching them in BatWerk (IntroHow do muscles workPainsMaintaining the routineHow to play). Interested? How would you like to improve it?

The Purpose

I believe that people can live healthier, happier and more meaningful lives with technological assistance. To wit, I believe _I_ can live a healthier, happier and more meaningful life with tech to help me. There's still some time left to live, and I'd rather live them feeling good instead of collapsing into a shambling creaky tangle of pain.

So I made a small free exercise app called BatWerk (Android / iOS) to keep myself from hurting at the end of the day. It's been working pretty well, back pains are mostly gone and neck pain is less frequent (and I have a script to deal with it). Better mood and sleep too. It's awesome (well, I would say that, wouldn't I?)

How does it work?

BatWerk challenges you to complete 12 rings over the day. Each ring takes about 2 minutes of easy moves to complete. Easy as in "Lift your arms to your sides ten times"-level of challenge. The moves are randomly picked from a selection of 40 exercises.

The trick with the rings is that the next ring unlocks on the next half hour. To complete all 12, you'll need to move a bit every now and then over six hours.

So it's a bit different from your run-of-the-mill exercise app. You use it constantly. It uses messaging to boost your mood. It doesn't really try make you sweat. Its goal is to periodically wake up your main muscle groups and maintain your mobility.

Obligatory screenshot from a version from 6 months ago.



How did this come about?

At first, the app was about doing this 15-minute workout. Just reminding you of the moves and the reps. Then taking you through a quest to the workout dungeon. Warmup on the afternoon fields, first set at the dungeon entrance, second set in the hall of warriors, third set in the volcanic mines, and the fourth set in the evil hall of lava with blood-red fog swirling about. After finishing the workout, you'd fly to the blue skies to do stretches.

It was good. But too much and too little at the same time. The workout needs a reasonably cool place, workout clothes, and half an hour of buffer for shower and changing clothes. It'll also beat you up pretty good, so you can't really do it several times a day (well, unless you're developing the app and need to test it.) Too hard and time-consuming, and it doesn't keep you moving throughout the day.

Back to the drawing board. Let's try an infinite sequence of random moves. Just open the app and move. Okay, this is easy and quick to do. I could do some of this every day. But, how often should you move? What's a good pattern?

According to Exercised, you should move a bit every half hour. And to flush the fast energy reserves in your muscles, you only need 15 seconds of anaerobic exercise. Right. Let's plug these together into a game where you need to move every half hour, doing about four different moves, 15-30 seconds each. Forces a refresh cycle for the muscles involved and doesn't take much time.

Still, well, you can't move _every_ half an hour. What about the meetings? What about lunch? Not to mention that 2 minutes of exercise every waking 30 minutes is pretty exhausting.

The current design is a game where you try to collect 12 rings over the day. This way you don't have to move every half hour, but it still drives you to have 6 active hours a day.

The 12-ring game was working OK for about 8 months, but now I've had some slowdown, hitting only 3-6 rings for a few weeks. Could be just the summer heat making exercise a bad idea in general, but I see it as something that could be fixed in the design of the activity. If the goal of the app is to keep you active every day, it really should keep you active every day, not just on days where you feel like it.

Still, 3 rings is way better than zero rings. Six minutes of exercise spread across the day. Without the app, I'd be at zero minutes. Still, it creates feelings of inadequacy, even with the non-blaming nature of the app. I did eventually break out of the funk and get a few days of full rings in a row. But I'd like it to rescue you a bit more. Get you back on track faster.


Blog Archive