Find yourself some pigs

I’ve heard this story first time from Yatin Gajjar, senior Sun Microsystems engineer in 2007 when I’ve visited their Palo Alto offices. We were talking about our very young startup at that time, and the story of pig and chicken was brought up. I never new the origins until I’ve found that the metaphor has been applied to agile development and scrum. Of course it makes perfect sense.

In the context of startups, having a strong core team is paramount. You cannot build anything just on your own. It is always a team. It is never an individual. The team that will push the development, company and community building activities forward, against the obstacles and difficulties. Pop culture glorifies individuals, because it makes it easy and we like the narrative about them. Teams are messy. Team dynamics is never something easy to talk about, never clean and tidy. But, there is this need for strong, core team where everyone can carry their own weight. This is something I’ve understood pretty early on. However, what I have discovered just recently is my inability to quickly distinguish between chickens and pigs. And this is extremely important skill to have if you are a founder. I guess it comes from experience and trying things out with multiple people on a number of occasions. It takes good people skills to be able to tell quickly who is who. It also relates to you as a founder. If you are not committed, you will end up doing what you do as a hobby, and you will never reach the critical escape velocity to take your venture off the ground. So be honest with yourself – are you a pig or a chicken in your startup adventure?

The bottom line is:

  • you have to be a pig; be honest with yourself
  • you have to find other, like-minded pigs, to help you pull it off; at least one; one is good
  • you have to avoid chickens, at least initially; chickens are not good core team material; they will be invaluable later on

Three-day monk

Sometimes I start something passionately, dedicate myself to the cause, and fully immerse myself in an activity. It can be anything – starting a new project, doing a proof-of-concept, or starting an essay for example. If a goal can be reached within one or two days, I often succeed. However, if the goal requires more time, it often drags for weeks, or years. More often it makes for an abandoned project.

Japanese have a phrase: 三日坊主(みっ・か・ぼう・ず)which roughly translates as three-day monk. It means exactly that. It means I am hyped up, fully committed, fully dedicated, but for a very short time. Only to fall back to my old ways and habits, and never achieve what I’ve set myself to achieve.

The way to avoid the three-day monk trap, is to dedicate  some time to the activity every day. To make it a daily habit. It does not need to be long amount of time – it can be short. But consistently bring the activity to the forefront of your mind, and to do it. Keep doing it. If you plan to blog or start new project – go to your blog and write a draft, or a beginning of a draft, or a first sentence. Start. Next day come back, add to it. Next day add more. You do not have to spent hours or days on an activity. You do not have to immerse yourself for extended periods of time. Doing it more frequently and consistently will bring better results.

 

Photo metadata analysis

Overview

Digital SLRs record a number of information (metadata) about the shot being made together with the actual image. This includes for example the aperture, shutter speed and the focal length that were used during the shot. This information is stored in an Exchangeable Image Format (EXIF) datastructure, that we can read, and process. If you have a large collection of photos, you can learn about certain facts that you may not know otherwise. I’ve done, for example, an analysis of my collection in the context of focal length I use, before deciding on what prime lens I will purchase.

Attempt 1 – nodejs with exif module

If you are not into nodejs and coffeescript, you may skip this section entirely.

Prerequisites:

  • nodejs
  • coffeescript
  • exif module (npm install exif)
  • fs module (npm install fs)
The script below can be executed and it takes variable number of arguments in a form Directory1 Directory2 …
The script will run through your directory and generate a comma-saparated-value file, with a date, aperture, focal length and exposure time, that then could be imported into Excel or used in Gnuplot.

ExifImage = require('exif').ExifImage
fs = require 'fs'

process_image = (img_filename) ->
 console.log 'Processing file: ' + img_filename
 new ExifImage { image : img_filename }, (err, img) ->
 if err
   console.log 'Error: ' + err.message
 else
   process.stdout.write tag.value for tag in img.exif when tag.tagName is 'DateTimeOriginal'
   process.stdout.write ', ' + tag.value for tag in img.exif when tag.tagName is 'ExposureTime'
   process.stdout.write ', ' + tag.value for tag in img.exif when tag.tagName is 'FocalLength'
   process.stdout.write ', ' + tag.value for tag in img.exif when tag.tagName is 'FNumber'
   process.stdout.write '\n'

process_directory = (dir_name) ->
 fs.readdirSync(dir_name).forEach (file) ->
 extension = file.split('.').pop()
 if extension.toLowerCase() == 'jpg'
 process_image dir_name + file

a = process.argv[2..]
a.forEach (val, index, array) ->
 console.log 'Processing directory: ' + val
 process_directory val

Unfortunately, even for a directory that contains 20 files this will not work. Your script will chew all available memory and crash. It will work for a directory with only few files though. Try it.

TODO: check how the existing exif module is implement it and see if it is possible to make it only use the EXIF record instead of loading the entire image into the memory.

Attempt 2 – bash and awk

After failing with the cool coffeescript-based histogram-drawing attempt, I’ve decide to quickly hack a command line bash script that would together with awk and command line exif program, achieve what I want: a histogram of all focal lengths used in a photo collection. This method is fast and reliable.

Prerequisites:

To do that for your collection, here’s what you’ll need

  • bash
  • awk
  • exif (or any other command line executable tool you fancy, that can read EXIF data)
  • (optional) gnuplot (command line plotting tool); you can use Excel or similar program, too.

If you do not have exif installed, and you are on macosx, to get exif you say: brew install exif (if you do not use homebrew, you should definitely give it a try; you’ll love it.).

If you on Linux, use your package manager and install exif. bash and awk come as default on both macosx and Linux.

First, we need to collect all the focal length from all the images in a given location. The following, will go into your specified DIRECTORY, and search for all files in the top level and all subdirectories. The command assumes that you store only images – if you want to limit it to .JPG or .jpg files only, check the manual for find command:

find DIRECTORY -type f -exec exif -m -t 'Focal Length' {} \; > mydata.txt

After that, you can have a look, and mydata.txt should contain something along the lines:

25.0 mm
20.0 mm
26.0 mm
16.0 mm
19.0 mm
39.0 mm
34.0 mm

Great. Now, we need to convert it to a histogram-like structure, so that we can visualize it on a graph. To do that, we’ll use cat and awk:

cat mydata.txt | awk '{count[$1]++} END {for (j in count) print j, count[j]}' > mydata

The above line will count each occurance of a given focal length, and combine it into a table. If you look inside mydata file, you should see something like that:

29.0 22
20.0 44
150.0 1
187.0 1
105.0 13
31.0 15

Now, with the mydata file, we’re ready to use gnuplot (or you can load the file into Excel). Simply fire up gnuplot, and do:

plot "mydata" w impulses lw 3 lc rgb "#00AA00"

Done. I hope you’ll enjoy checking what is the focal length you love to shoot at.

Check examples of focal length histograms of my photos.

 

What defines you

Direct vs. Indirect control

I often spend a lot of time planning, envisioning, and wishing for a particular outcome to happen. It occurs in different areas of work – in software projects, in design, in management. This is normal. This “wishful thinking” gives us direction and focus. It helps to identify elements and activities that shift odds in our favour. This “wishing” helps to create a plan. It does help, indirectly, to achieve our desired outcome. And so on. This is how we all get some things done. We “wish” for something to happen, we do “our thing”, and then the outcome happens. As we wished for, or not.

Often, the outcome decides if the undertaking is considered a success or a failure. This is a form of mental jump that we do. A form of “simplified thinking”. The trap is that our “wishful thinking” and “outcome” blur the value of our work with all the value that is created in spite of us. Our mental processes blur the elements that are in our direct control from the elements that are not in our direct control.

Craftsmanship

One realisation that occurred to me is that things that are in my direct control are, by far, much more important than things that are not. And, what’s more important, things that are in my direct control define me, whereas things that are not in my direct control, do not. What follows from this is that the outcome alone does not define me. This is somewhat counter-intuitive and a bit entangled, so let me explain. I work on a project. I control a number of elements, e.g. the quality of the code, the usability of the interface, the architecture of the system, the engagement of end-users, and so on. Those things are in my direct control. These factors shift the odds of the project being adapted and used by users. But, I do not control directly the end-user uptake. I can only, indirectly, influence it, and I can shift the odds by doing high quality work, but there is a number of elements that will always be beyond my control. The trap is that I often disregard those elements, and treat them as if they were non-existant. This is a mistake for two reason: first, I lose focus on things that are in my control, and second, I tend to take credit (or blame) for things that were completely out of my own control or influence (things that just happened, in spite of my own efforts or doing).

The result we wish for is often a combination of elements in our direct control, together, with elements that are outside, elements that we do not control directly. Focusing only on the outcome might distract you from the quality of your work. Instead, you should direct your focus to all the elements that are in your direct control.

Instead of the outcome, you should let the quality of your craftsmanship define you. 

The interesting paradox is that the outcome of a project is strongly influenced by the quality of work and all the elements in our direct control. Hence, the elements in our direct control and the actual outcome are entangled. Focusing on the mastery and the craftsmanship will ultimately help to achieve the desired outcome and it will prevent taking blame (or credit) for things that are beyond our control.

 

Programming: the art of writing

To some people, especially those who cannot program computers, the art of computer programming seems more like “monkey and typewriter” type of activity. This misconception is far from the truth. Computer programming is a complex and highly rewarding intellectual activity, much closer to writing a novel, than conducting an engineering task. The art of computer programming always appealed to me — it is a vast land somewhere on the intersection of science, mathematics and artistic expression of myself. Focus, creativity, discipline, and an inner sense of beauty, all have to come together, for the final piece to be created.
Sure, there is a lot of bad software around. Same as there is a lot of bad novels or films. Especially those created by committees and executive boards. There is, however, a growing number of small indie developers writing high quality, beautiful software.

Stephen King said: “The scariest moment is always just before you start.

Programming is a skill, a tool, that you can use to express yourself. It is a way of being in the world. How to get started? First, you have to master the craft itself. Simply write. Write code. Write software. For yourself, for your mom, for your friends. Keep writing. Anything. In any language there is. There is no other way but doing it. Start small. Tasks that can be achieved in under an hour will work for you like etudes for musicians. Practice.

“Talent renders the whole idea of rehearsal meaningless; when you find something at which you are talented, you do it (whatever it is) until your fingers bleed or your eyes are ready to fall out of your head. Even when no one is listening (or reading, or watching), every outing is a bravura performance, because you as the creator are happy. Perhaps even ecstatic.”

Then, once you grow and become more confident, plan bigger. Plan a novel or plan a symphony. There is lots of parallels between writing computer code and the act of writing, in general. You need to have the concept in your head. You need to feel it, you need to know what it is. Then, you need to organise a routine, a discipline, and let the creativity take over.

An excellent and accomplished writer, Stephen King, has written a spectacular book “On Writing: a memoir of the craft“. The book provides many insights into the process of being creative, and how to write. It applies to computer programming, too.

“There is a muse, but he’s not going to come fluttering down into your writing room and scatter creative fairy-dust all over your typewriter or computer. He lives in the ground. He’s a basement kind of guy. You have to descend to his level, and once you get down there you have to furnish an apartment for him to live in. You have to do all the grunt labor, in other words, while the muse sits and smokes cigars and admires his bowling trophies and pretends to ignore you. Do you think it’s fair? I think it’s fair. He may not be much to look at, that muse-guy, and he may not be much of a conversationalist, but he’s got inspiration. It’s right that you should do all the work and burn all the mid-night oil, because the guy with the cigar and the little wings has got a bag of magic. There’s stuff in there that can change your life. Believe me, I know.”

The advice on making a first prototype (or first book draft as he calls it):

“I believe the first draft of a book — even a long one — should take no more than three months…Any longer and — for me, at least — the story begins to take on an odd foreign feel.”

And, how te evaluate your proof-of-concept:

“Write with the door closed, rewrite with the door open. Your stuff starts out being just for you, in other words, but then it goes out. Once you know what the story is and get it right — as right as you can, anyway — it belongs to anyone who wants to read it. Or criticize it.”

Talking on entrepreneurship

Murad Kamalov research seminar talk “Is entrepreneurship an option for University Graduates?”

Last month, Tomek and I were hosting in Dunedin a researcher and entrepreneurial from Helsinki Institute for Information Technology (HIIT) and Aalto University, Murad Kamalov. Murad is Ngarua‘s collaborator and co-founder of doThinger. He has spent 4 weeks developing functionality and talking about startup life and entrepreneurship in a number of venues around Dunedin. One of the presentation he has done was during the monthly CodeCraft meeting, and another during the weekly research meeting of the New Zealand Distributed Information Systems group.

Murad has discussed the history of entrepreneurship activities in Aalto University and how a group of students started a movement that results now in initiatives that raise multi-million investments and engage over 400 participants in the launch events. He has discussed how StartupSauna and Garage48 work. Getting a successful startup off the ground requires more than just pure technological excellence. It requires the environment in which strong teams of complimentary skill sets can be established, it requires proper mentoring and support environment, experimentation and exploration.

We believe entrepreneurship and startups can be a valid option for university graduates, especially those with technical backgrounds and ability to develop proof-of-concept ideas rapidly using in-house skills. What do you think?

 

Deploying nodejs application on Apache web server

In this post I will explain how to deploy your own NodeJS application on Apache server. I’ll also briefly discuss how to use supervisor to manage your multiple node-driven applications.

There are many possible ways for deploying your node apps and redirecting input/output streams from a node process to your Apache/nginx web server. In this post we use a simple python tool called: supervisor. To use supervisor, we need to setup a startup script for our application, install and configure supervisor, and configure VirtualHost on the apache or nginx web server.

start.sh script
Create a simple Bash script, called start.sh, in the root directory of your application. This script will start up your application.

#! /bin/sh
cd /var/www/vhosts/{my_first_app.com}/{path_to_my_first_app}
/usr/local/bin/node app.js

Make sure that the path to node and the path to your application folder are correct. Test by executing the start.sh script.

supervisor install and configuration
supervisor is a tool that provides an easy way for managing processes. Installing supervisor is easy. You simply follow your standard python package manager installation procedure: eg. easy_install supervisor or pip install supervisor

To configure supervisor first generate the standard configuration file:
echo_supervisord_conf > /etc/supervisord.conf

and then fill-in appropriate fields to match to your application paths:

[program:my_first_app]
command=/var/www/vhosts/{my_first_app.com}/{path_to_my_first_app}/start.sh
autostart=true
autorestart=true
startretries=3
stdout_logfile=/var/www/vhosts/{my_first_app.com}/{path_to_my_first_app}/log/server.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
stderr_logfile=/var/www/vhosts/{my_first_app.com}/{path_to_my_first_app}/log/error.log
stderr_logfile_maxbytes=10MB
stderr_logfile_backups=5

To launch the supervisor you run supervisor command. To check the status of your managed application, use supervisorctl These executables live in your python path (where your particular python package manager installed them).

Apache VirtualHost configuration
Inside your VirtualHost entry, setup the following:

ServerName myfirstapp.com:80
ServerAlias *.myfirstapp.com
DocumentRoot /var/www/vhosts/my_first_app.com/my_first_app/public
AllowOverride FileInfo Indexes
RewriteEngine On

# redirect all non-static requests to node
RewriteCond %{DOCUMENT_ROOT}/${REQUEST_FILENAME} !-f
RewriteRule ^/(.*)$ http://#{server_ip}:#{application_port}%{REQUEST_URI} [P,QSA,L]

 

Full deployment automation
With the above instructions you have to manually copy, build and deploy your application into the appropriate directory on the server, in the examples above, at /var/www/vhosts/{my_first_app.com}/{path_to_my_first_app}

This is quite a lot of work, and ideally, a single push to a remote repo should update your application deployment on your production web server. To achieve that, we need automation. I will explain it in the next post – stay tuned.

Physical server room.

Our server room, also known as an office, has been emptied yesterday. We have migrated all our infrastructure to the hosted managed solution, and moved out of the physical room. None wanted to work in there. No windows, cramped, poor lighting, and isolation provided somewhat spirit crashing atmosphere. It was great with few people in there – we had good times on meetings and various celebrations. But the costs of keeping physical server room vs. managed hosted solution outweigh the benefits.

We have been in Centre for Innovation for about 5 years. That’s a very long time, and a lot of things happened in there. The most important milestone was the lesson of agility and outsourcing. Moving out and moving on. Not easy.

Advanced topics for Android developers

Google I/O talk notes.

Dealing with various APIs and Android versions.
Coding patterns.

How to deal with various device capabilities.

private static boolean isNewAPI =
   android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Intent intent;
    if (isNewAPI) {
        intent = new Intent(this, ModernActivity.class);
    } else {
        intent = new Intent(this, LegacyActivity.class);
    }
    startActivity(intent);
    finish();
}

Sensor registration example (Orientation Sensor).

private static boolean isNewSensorAPI =
   android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.CUPCAKE;

boolean gyroExists = getPackageManager().hasSystemFeature(
  PackageManager.FEATURE_SENSOR_GYROSCOPE);

IOrientationSensorListener myListener;
if (gyroExists) {
    myListener = new GyroOrientetationSensorListener();
} else if (isNewSensorAPI) {
    myListener = new AccOrientationSensorListener();
} else {
    myListener = newAccOldOrientationSensorListener();
}

myListener.setOrientationChangeListener(myOCListener);

Getting users feedback

try {

} catch (Exception ex) {
   String exText = "something went wrong";
   MyApplication.getInstance().tracker().trackPageView(exText);
}

A – B user testing on Beta deployment. Getting real users feedback on different possible scenarios/visuals/etc.

private static final boolean isA =
UUID.randomUUID().getLeastSiginifcantBits() % 2 == 0;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if(isA) {
       setContentView(R.layout.mainA);
       MyApp.getInstance().tracker().trackPageView("/AUser");
    } else {
       setContentView(R.layout.mainB);
       MyApp.getInstance().tracker().trackPageView("/BUser");
    }
}

Using Android Market for beta testing:
- private betas that require login or passcode
- public betas with disguised or obscured listing
- update listing or create new on launch
- point beta users to official app at launch
- be clear that this is beta. venues for feedback
- make sure you translated your app name and description to local languages

Other hints:
- upload your package before distributing it to anyone
- backup your keystore
- use generic gmail account for app releases (sharing the responsibilities)

Sensor orientation tip

int x = AXIS_X;
int y = AXIS_Y;

case (Display.getRotation()):
    Surface.ROTATION_0: break;
    Surface.ROTATION_90: x = AXIS_Y; y = AXIS_MINUS_X; break;
    Surface.ROTATION_180: y = AXIS_MINUS_Y; break;
    Surface.ROTATION_270: x = AXIS_MINUS_Y; y = AXIS_X; break;
    default: break;
}
SensorManager.remapCoordinateSystem(inR, x_axis, y_axis, outR);

Generating unique identifier (per user/device). Use the following method of generating the unique id and store it in users’s preferences.

UUID. randomUUID().toString().

How to get the up-to-date location without draining the battery?
Use passive location provider, or, alternatively, use Location change intent.

final int resultCode = 0;
final String locAction = "com.ioApp.LOCATION_UPDATE_RECEIVED";
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
Intent intent = new Intent(locAction);
PendingIntent pi = PendingIntent.getBroadcast(this, resultCode, intent, flags);
locationManager.requestLocationUpdates(provider, minTime, minDistance, pi);

....

BroadcastReceiver locReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context ctx, Intent intent) {
    String key = LocationManager.KEY_LOCATION_CHANGED;
    Location location = (Location)intent.getExtras().get(key);
    // process new location
  }
};
IntentFilter locIntentFilter = new IntentFilter(locAction);
registerReceiver(locReceiver, locIntentFilter);

To use intents to passively detect location changes start a service to refresh the data without updating a UI.

 receiver android:name=".locReceiver" android:enabled="true"
 intent-filter>
  action android:name="com.ioApp.LOCATION_UPDATE_RECEIVED"/>
  /intent-filter>
 /receiver

Wake alarms and non-waking alarms example:

int wake = AlarmManager.ELAPSED_REALTIME_WAKEUP;
int sleep = AlarmManager.ELAPSED_REALTIME;
long minInt = AlarmManager.INTERVAL_HALF_DAY;
long bestInt = AlarmManager.INTERVAL_HALF_HOUR;
long trigger = SystemClock.elapsedRealtime() + bestInt;

alarms.setInexactRepeating(wake, trigger, minInt, alarmIntent);
alarms.setInexactRepeating(sleep, trigger, bestInt, alarmIntent);

Make your data updates smart by monitoring number of elements. For example:

monitor connectivity

Connectivity Manager cm = (ConnectivityManager)ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNet = cm.getActiveNetworkiInfo();
boolean isConnected = activeNet.isConnectedOrConnecting();
boolean isMobile = activeNet.getType() ==
   ConnectivityManager.TYPE_MOBILE;

monitor power and battery

IntentFilter bf = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent bat = ctx.registerReceiver(null, bF);
int bstat = bat.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean power = bstat == BatteryManager.BATTERY_STATUS_CHARGING ||
    bstat == BatteryManager.BATTERY_STATUS_FULL;

monitor docking status and other possible state-change broadcasts to monitor:

ACTION_DOCK_EVENT
ACTION_BATTERY_LOW
ACTION_POWER_CONNECTED
ACTION_POWER_DISCONNECTED
CONNECTIVITY_CHANGED

Cloud-based Android Backup service.

Backing up shared preferences

public class MyPrefsBackupAgent extends BackupAgentHelper {
  static final String PREFS = "user_prefs";
  static final String PREFS_BACKUP_KEY = "prefs";

  @Override
  public void onCreate() {
    SharedPreferencesBackupHelper helper =
       new SharedPreferencesBackupHelper(this, PREFS);
   addHelper(PREFS_BACKUP_KEY, helper);
  }
} 

in Manifest
application android:label="MyApp" android:backupAgent="MyPrefsBackupAgent">
  meta-data android:name="com.google.android.backup.api_key"
          android:value="MyAPIKey" />
/application>

Making your apps more adaptive. Use appropriate keyboard for EditText entry UI.

eg, in EditText  ....
   android:inputType="phone"
   android:imeOptions="actionSend | flagNoEnterAction"
/>

Listen for those special action keys in your OnEditorActionListener:

EditText.OnEditorActionListener myActionListener =
 new () {
   @Override
   public boolean onEditorAction(EditText v,
     int actionId,
     KeyEvent event) {
       if (actionId == EditorInfor.IME_ACTION_SEND) {
         //  handle the SEND action
         return true;
       }
       return false;
    }
};

editText.setOnEditorActionListener(myActionListener);

Handle Audio nicely (interoperate nicely with audio focus).

Make everything asynchronous and make use of background threads (Handler, AsyncTask, IntentService, AsyncQueryHandler, Loader and CursorLoader).

CursorLoader example

// within onCreate...
getLoaderManager(),initLoader(0, null, null);

// Callbacks
public Loader onCreateLoader(int id, Bundle args) {
    Uri baseUri = MyContentProvider.CONTENT_URI;
    return new CursorLoader(getActivity(), baseUri, null, null, null, null);
}

public void onLoadFinished(Loader loader, Cursor data) {
   mAdapter.swapCursor(data);
}

public void onLoaderReset(Loader loader) {
    mAdapter.swapCursor(null);
}

Testing/Debugging. Use Strict Mode:

public void onCreate() {
 if (DEVELOPER_MODE) {
   StrictMode.setThreadPolicy(
     new StrictMode.ThreadPolicy.Builder()
              .detectDiskReads()
              .detectDiskWrites()
              .detectNetwork()
              .penaltyFlashScreen()
              .build());
 }
 super.onCreate();
}

Cleaning up MacPorts

First, let’s see how much macports use up on my system:

$ du -sh /opt
8.1G

Next, how many packages do I have installed in total:

sudo port installed | wc
     538

And how many packages are active:

sudo port installed | grep active | wc
     253

Now let’s trim the entire collection, by uninstalling all inactive packages:

sudo port uninstall inactive

If you have some dependency issues, add ‘-f’ flag to the port command that will ignore dependencies.

This reduced the disk usage in /opt to 4.8G, and got the number of all installed ports to 254.

More cleanup:

sudo port clean --all installed

This removes all temporary files generated while building and installing packages. After that, I got down to 3.6G in /opt. Not bad down from 8.1G.