Separate development feedback and performance evaluation

Development feedback—intended to help team members learn and improve—needs an environment where the recipient can absorb information. This requires psychological safety: no blame, no pressure, no personal attacks, no consequences, no reason to get defensive.

Performance evaluation—informing employees how happy the company is with their output—is a high-stakes and high-stress situation that precludes learning.

While both are important, they're distinct types of communication:

Development Feedback Performance Evaluation
Goal Learning and information gain Assessment and rating
Scope Feedback on a specific action, close in time to the actual performance Feedback on performance over a longer duration
Frequency Daily-weekly: whenever reviewing a report's work output Quarterly/yearly or on-demand when performance problems arise
Level Often about details, such as: missing error handling code, a better way to do X, better wording for customer communication Higher-level, such as: progress toward yearly goals, or work output over the last weeks
Stakes Low stakes: "you are not your code" High stakes: career progress or negative consequences
Formality Informal and usually verbal More formal and usually documented in writing
Consistency Individual feedback for each team member Fairness requires consistent success criteria for all team members
Preparation Little or no preparation necessary: a casual discussion Fair evaluation requires research and preparation

It's easy to slip a performance remark into a feedback discussion, but it's a mistake. The employee will focus on the performance aspect and not on learning.

Off-hand, negative performance evaluations are especially damaging. They encourage a defensive, argumentative mindset which precludes learning. They also risk escalating into a larger performance discussion you're not prepared for.

Here are some examples of what to avoid:

  • BAD: Your code is missing error handling. You seem distracted recently.
  • BAD: Your presentation was hard to follow. This way you'll never reach your sales quota.
  • BAD: Your customer email sounded rude. I'm fairly unhappy with your work at the moment.

If general performance concerns arise during a feedback session, wrap up as usual, schedule a separate performance talk, and prepare accordingly.

An example of a better approach:

  • GOOD:
    • Feedback session: Here are some points you could improve in your project proposal: «detailed explanation».
    • Performance discussion a day later in private: I had the impression that your recent work is less polished then it used to be. Is everything alright?

Decide up-front which discussion you will have: feedback to help your reports grow or performance evaluation. Avoid mixing the two and pick the right approach and setting for each.

Git Best Practices

Here's a list of Git version control best practices I recommend.

Staging changes: git add -p and git diff --cached

Use git add -p to stage changes to commit. It displays each change individually and asks whether to include this part in the commit or not. It thus gives control of the contents of the commit.

Similarly, use git diff --cached to review the staged changes before committing.

I'm a big fan of both methods. They force me to code review my own commits and make sure there is no commented out code and no spurious whitespace or formatting change. More importantly, this approach finds countless bugs! Re-reading the diff is a great opportunity to ask questions like "is this really correct," or "does it cover all corner cases."

Rewriting history: git commit --amend

When discovering a problem with the most recent commit, make your changes, stage them, and use git commit --amend to fix the problem directly in the previous commit. This helps to keep a clean history.

A note on changing history in git: Altering commits that were already pushed to others is considered a bad practice, because it breaks work that others might have based on top of the now-obsolete commit. In most situations, I agree with this assessment. However, the risk is manageable when you're working with a small team. Especially for feature branches that are unlikely to be the basis for further work, I tend to advocate rebasing / amending pushed commits to keep a clean history. Just make sure that it's OK with the team.

Rewriting history: git rebase -i HEAD~10

Interactive rebasing allows reordering or fixing past commits and generally reworking recent commit history. See the Git documentation on rewriting history for in-depth explanation.

The HEAD~10 part tells git to look at the 10 previous commits, which often is a sensible default.

Also note the "changing history" section above.

Atomic commits

Commits should contain one logical change. Avoid having multiple, independent changes in a single commit. Why? It's easier to review and understand multiple smaller commits than one big commit. It's easier to merge commits together than to split one big commit. If bisecting found that a bug originated from a commit, it's easier to identify and fix the issue in a small commit than in a big commit.

git add -p is great for creating atomic commits from a source tree that already has several changes.

Commit messages

Three key points:

  • Form: Write a proper subject line, separated by a blank line from the rest of your commit message.
  • References: Include a link/ID of the relevant task in your ticket system.
  • Content: Write for your future self. One year down the line questions like "why did we want this / why did we choose this particular approach / what alternatives did we consider" are going to come up. Between your commit message and your ticket system task description, you should be able to answer those questions.

Git branching patterns

The branch structure of a git repository should adapt to the development activity:

  • If several people work on the same repository at the same time, you'll need feature branches.
  • If you have to support several releases in parallel, you'll need release branches.
  • If you need frequent updates of your releases, you might need hotfix branches as well.

The large number of branches created with these schemes is unavoidable in heavily used repositories. However, the added complexity makes the commit history hard to understand.

For less active repositories, such as small tools or legacy software, simpler approaches are better suited. Often a single main or master branch together with occasional feature branches is enough.


If you have other Git best practices to include, please comment below.

published October 04, 2022
tags git

Covid variant competition and a longer-term strategy

How do new Covid strains replace older ones? A simple model provides some answers:

Infection numbers timeline: one strain out-competes another

The model consists of three rules:

  • At the beginning only the base variant exists, until on day 200 the Omicron variant appears.
  • Each base variant infection causes 1.25 follow-up infections. Each Omicron infection causes 1.40 follow-up infections.
  • A lockdown starts when the number of sick people reaches 20000. This results in a reduction of new infections by 36%. The lockdown ends when the number of sick people falls below 6000 again.

This might be the world's worst Covid model, but it gets one thing right: the competition between variants. It shows the dynamic:

Normal life: numbers rise, Omicron grows faster, total numbers mandate a lockdown; Lockdown: numbers fall, base variant shrinks faster, the lockdown ends

Earlier variants vanish due to the stronger human interventions required to keep the newer variants in check. We decide on preventive measures based on total numbers, not per-variant details. More infectious strains require longer, more frequent, or more stringent lockdowns. These measures hit older variants harder, which makes them recede.1

At some point we might not be able to enact strict enough countermeasures to prevent newly evolved strains from spreading. Arguably, this already applies to Omicron. If not, new variants will continue to emerge. Our lukewarm vaccination efforts exacerbate this issue by applying evolutionary pressure on the virus. Vaccination rates are high enough to stop earlier strains, but low enough to allow the spread of more infectious, more vaccination-resistant variants. Thus, we should expect to see more problematic strains in the future.

Where does this leave us? Covid is on track to become endemic, so everyone will get infected eventually. The big question is how to manage this process without overwhelmed hospitals, and ideally without endless lockdowns. Vaccination is a key ingredient, since it reduces hospitalization and death rates tenfold. A 100%-vaccinated society could support ten times more infections at the same time.2 During Corona peaks, masks, social distancing, and lockdowns help to remove pressure from hospitals. Outside of peak seasons, a relaxed approach is probably best.


1 Does this argument still hold despite the lower hospitalization rates of Omicron patients? It does: avoiding overwhelmed hospitals still requires strict Omicron countermeasures, just at a higher number of infected people.
2 Compared to an unvaccinated society. Countries with a 65–75% vaccination rate can afford 3–4 times the infections as before vaccination campaigns. The comparison of Covid numbers of winter 2020/2021 and winter 2021/2022 supports this.

published January 16, 2022
tags health

How to create pixel-perfect PDF reports in 2021

Web development projects often require printable reports or invoices, but relying on the browser's built-in print function is a frustrating experience. Results vary with operating systems and render engines, leading to frequent bugs and broken layouts. Worse, fixing them risks degrading other platforms.

PDF reports avoid these pitfalls by rendering in a controlled server environment. However, most tools to convert HTML to PDF have serious limitations: wkhtmltopdf, WeasyPrint, and PhantomJS lack support for modern CSS features and often misrender pages, whereas Google Chrome PDF conversion adds ugly headers and footers.

Puppetteer, the successor to PhantomJS, solves these issues by bundling its own Chromium browser. It supports pixel-perfect PDF generation and respects CSS page margins, allowing full control over the PDF page layout.

My htmlpdf.js command-line tool automates HTML to PDF conversion:

npm install puppetteer
./htmlpdf.js input.html output.pdf
./htmlpdf.js output.pdf

Give it a try and let me know what you think.

published September 07, 2021
tags web

Capture Bluetooth Traffic from Android

Bluetooth devices have become ubiquitous, whether it's headphones, heart rate monitors, smart watches, or light bulbs. While many of them come with mobile apps for control and configuration, sometimes it's convenient to access them from a computer instead. If this is not supported out of the box, recording the Bluetooth traffic is useful in order to create a custom tool.

Helpfully, newer versions of Wireshark include an androiddump utility to capture Bluetooth traffic directly from Android phones. The setup couldn't be simpler: connect the phone to the computer via USB, enable USB debugging, select the "Android Bluetooth" capture source in Wireshark, and start recording!

Android also has a function to log all traffic into a file, which can later be copied to a computer for analysis in Wireshark. This is similar to creating a tcpdump of TCP/IP traffic, just for Bluetooth. Unfortunately, the location of this log file changes between Android versions, and access seems to require root in recent versions. One reliable method I found was:

  • Enable "Bluetooth HCI snoop log" in the Android developer options.
  • Connect to the phone via USB debugging with adb
  • Run adb root to gain root-level access to the phone
  • Run adb bugreport bugreport.out to create a bug report ZIP file. Either this contains the Bluetooth log file directly (search for a btsnoop_hci.log), or the full path of the log file (grep the bug report for btsnoop_hci.log)
  • If the log was not contained in the bug report, get it via adb pull <full_path_to_logfile>