Saturday 13 July 2024

What is it with the AMD Adrenaline Software?

Very quick one, I've taken delivery of a new Graphics Card... (oooooo ahhhhh).

And it's a seed change for me as this is my first ever AMD graphics card, so this is of course going in my own main PC, which itself is my first ever AMD CPU powered machine I am therefore all Team Red for the first time ever... EVER!
I have been so excellently impressed with the performance of the Ryzen Zen 2 chip at the heart of my machine and my old EVGA 1080 GTX Superclocked was showing its age.  It was therefore time.

Unfortunately I made a huge mistake... No, it wasn't the model, no it wasn't the price, no it wasn't even paying to import it (because it was cheaper by a margin to have the card fly all the way to me through customs from California than actually buy it here in the UK - Thanks Brexit! - Not).

Anyway, it arrived and I plugged it in, and my monitor didn't come to life; I then found the signal was going to my massive 4K TV (on the HDMI) not my monitor (on the display port - go figure).

That sorted I then needed drivers, so I went to the AMD site and looked and found what it said was the best package to install on Windows 10 64bit for this card....

I don't know ANYTHING about the AMD software, and oh boy is that a mistake.

I installed it and was immediately REALLY REALLY IMPRESSED with it!  Yep, it's beautiful (even if it is backed by Qt and I hate Qt) and smooth and does wonderful things.

Unfortunately, and this is key, it was taking a lot of CPU whilst doing this work... So I closed it... and it STILL took a bunch of CPU, nearly 10%!

Just idle at the desktop, 10% CPU.

I was properly baffled by this, it must be doing something, so I went through all the settings and disabled everything I could see, everything turned off, clean reboot, and it was still taking 10% but now was peaking to 14% on odd occasions just idle!

What the heck, a bunch of goodling and I can find a bunch of folks complaining about exactly this, and one guy properly threatening to boycott their brand unless they explain how to just install the drivers without this software suite.  There was no answer to these please, and they were old posts, like there's been an apparent hard reset of the search results for this kind of question.

I spent a bunch of time trying to fathom this issue to no avail, so I like my forebears in the search results set about trying to find JUST a driver package.  I could not find one,

I therefore just decided to pull the card and possibly return it.  And so uninstalled the AMD Adrenaline Software, it took its time... And really grated on me as it spent about two minutes showing me the text "We value your feedback"... without ever giving me a link or contact in order to tell them anything... and this is during an uninstall process; surely someone when they added this eye stabbing annoying message thought "Oh we need to have them able to give me feedback!" ... Nope, seems not.

Now completely uninstalled and the package opens a website... This website... which I link only for you to see... as it's an advert... for a version of the very card I am uninstalling the software for.... Yes, AMD that's pretty tone deaf, also... WITHOUT YOUR DRIVERS INSTALLED YOU CAN NOT VIEW THAT SITE, so double double own goal there .... Less Advanced Micro Devices and Anyone Might Despair.


Here I am then, flabbergasted, back to a tiny resolution, opening chrome and trying to find just a raw driver package and I can't find one.  My machine has now cooled and I hear the CPU pump has ceased whirring away, as it was constantly with the 10% load on it!

I sat looking for ages when I first installed the card, really ages, we're taking an hour before I went with this Adrenaline stuff.

When suddenly Windows itself kicks into life and installs the Microsoft supplied WHQL driver, and guess what?  Yes, it's the same base driver, and it works absolutely brilliantly... and crucially my machine is not taking any CPU when idle, it's gone quite quen idle, I'm sat now typing this with a YouTube video playing, about 6 chrome tabs open and a copy of Visual Studio Code open too and it is silent, the machine is silent, just as I built it to be (unless under load).

Looking about I see AMD are recruiting software engineers, I have no idea what for, but if they see these pages - I'd suggest a bunch of refactoring is due over there folks.

Tuesday 9 July 2024

Code Locality & Concurrent Systems

Let us talk Software Design, let us talk about code locality.  What do I mean by locality?  Well, in Software Engineering we often talk about code being self documenting, a fabulous place to be if the code performing the work is right in front of you.  But to be honest systems get pretty big pretty quickly.  So you're very much more likely to me making calls to API's or just bunched of functions you have to blindly trust, unless you have the leisure of digging into them.

And there's usually precious little time in development which is invariably taken up with writing new code, not going over the old (unless you're very lucky - but that's a conversation for another day).

Now, these API's can encapsulate large features, and they don't always achieve the amount of descriptive power we'd like in the call site location we're at.  I therefore advocate for a comment around that point.

And so we are immediately at odds with the wish for our code to be as accessible, local, as it can be, but also encapsulating the large unrelated tasks elsewhere.  For me this is the dichotomy of code locality in a nut shell.

Where can it come unstuck?  Well, back in the day (and I have to be honest with most engineers still thinking linearly even today) you could be forgiven to writing out your big system block diagram, connecting things with events or signals and just going about your business, when your code called "BigFooFunction" in "BarBarBlackBox" library you didn't pay it much mind.  Today however, as Moore's Law runs aground on the rocks of pumping ever more cores into a system we have to think in concurrent terms and it is in just such a scenario that I want to pick up thinking about Systems Design and Code Locality.

Let us perform a thought experiment.  We have a system which progresses from Eggs to Birds, it controls the state of the entity laid as an egg, being incubated, then hatched, fed and tended until they start to fledge and ultimately turn into a bird.  All this transmogrification of the state from Egg to hatchling to fledgling to bird happens asynchronously in the background in a big slap of system you do not have to worry about.

All you worry about is a signal coming into you saying "predator", and when this signal arrives you need to stimulate all the little birds you have into action, they all need to take flight.

for(auto& bird : flock)

{

   bird.TakeFlight();

}

This is the locality of our change to the property flight on each bird, we are absolutely unaware of each individual possible state the bird can be in and so we rely on the API and code backing our call to "TakeFlight".

Now, let us assume the members of "flock" are all of the base type "Bird", so they all have a TakeFlight function?  Well, they might, if Bird looked something like this:

class Bird

{
   public:

       virtual void TakeFlight() = 0;

};

Then at compile time all the derived classes would have to implement "TakeFlight".

class Egg : public Bird

{

    private:

        uint64_t mTimeLaid;

        float mTemperature;

    public:

        void Incubate();

        void TakeFlight() override;

};

And because we know this is an Egg we know it can't fly and so we know that this override of the function will do nothing.

This is perfect code-locality for that derived type, but for bird itself it leaves us the open question, well what does it do?  Does it do anything??  Should "TakeFlight" return us some code to indicate the bird took flight and an error if not, but an egg not flying is not an error, so must it be some compound it returns.

Now, I am straying into API design somewhat, and they are related fields, but really here we're thinking about where the active code sits, what is its locality compared to our callsite in the loop?

And for a function, you can see we can define this and know.

Now lets us change our example:

class Bird

{
    public:

       bool mInFlight { false };

};

and

class Egg : public Bird

{

    // As above

    public:

        void TakeFlight () override { 

            // Intentionally Empty 

        }

};

Our egg can not fly, or can it? for now with a public member we're flying somewhat in the wind of a contrived example, but anything can now set the value of mInFlight, our for-loop upon the predator signal can now achieve its aim:

for(auto& bird : flock)

{
   bird.mInFlight = true;

}

And this is correct, this loop in review would pass, who can argue?  The bird took flight, it did, it is true.  And this is code locality in action, for at the location we needed it the functionality was available to mutate the value and achieve our goals, no matter what the state of the rest of the system was.

This is a very dangerous place to be.

Especially with an asynchronous system, lets say this is not a trivial call, lets say that the call site is to loop through a series of resources and start them loading, and upon that call each is pending load, but not yet loaded?

for(auto& item : objects)

{
    item.StartLoad();

}

We can assume this code is starting the load, but now lets package this into some context, we like to have our code be self-documenting after all.

class Loader

{
    private:

        bool mEverythingReady { false };

        std::vector<Objects> mObjects;

    public:

        void Load()

        {

            for(auto& item : mObjects)

            {

                item.StartLoad();

            }

            mEverythingReady = true;

        }

   };

Can you already spot the problem?  The local code here is communicating that EVERYTHING READY, when it is anything but that, you have simply started some other action elsewhere, you have not checked the state, you have not deferred until ready, you have started load and that is all you know, but you code here locally is communicating something subtly different.

And in huge systems you must not fall foul of this kind of behaviour, you need your code locally to communicate what it intends, to do as it intends and if you spot silly public interfaces like this do not be affraid to fix them, the bravery to address an issue, if only to raise it to the owner, is a step in the right direction with massive software systems.

Monday 1 July 2024

Operational Embarassment

Today I bring you a story, a true story, from my very own past.  You may have seen the "broken feet" link on this very page, so yeah I broke both my ankles at the same time falling off a roof.

Fast forward just under two years and I need an operation as the joint are going bad.

On the day of the operation I am handed a lovely double hospital gown and the lady checks who, what and why I am there... The surgeon see's me and puts a big big black arrow on my right leg and I am shown into the pre-op waiting room to just wait.

I am the first case, but they want to pre-op everyone, and slowly the waiting room fills with men in the same garb.

No-one speaks.... No-one.

This is quite strange with us blokes, and to be honest I find it a little freaky, as I'm sat there with my big black felt arrow on my ankle I just joke... "Bet we can guess what I'm having done".

ALL OF THEM, and there's like six guys now, all stony faced.  They don't look at me, they don't look at one another.... Nothing, nada, zip.

What the heck?

So I'm up first, off I go, limping out and am taken to theatre.

I am a quirky case, as not only am I first, but I am to be awake during the procedure, I get an epidural and then a tourniquet is applied and I get to watch them work on my ankle in this freezing cold room.

An hour or so later I'm in resus, first in, first out... I am surrounded by empty bays.  Slowly as the morning wears on and my leg starts to awaken in cramping pins and needles the first of the other guys is brought through... No idea what procedure he's had.

Then another.

Then another.

Until all six bays around me are full and I'm not allowed a drink, some toast and encouraged to rub some life into my leg.

I am very clearly the only patient in this state, the others are all coming around from general anesthetics.

My surgeon pops to see me, he's very happy, I'm very happy and sure enough in the next hour I get up and for the first time since the original accident I'm not in pain in my ankle!

These other guys then start to awaken, and they don't want the nurses helping them, the female nurses, they're all deadly coy and a bit embarrassed, as I'm up and about moving my leg and getting it wrapped and a cast applied.

 "Sorry"

This voice comes across the ward.

 "Sorry for ignoring you earlier, you were just breaking the ice"

And we have a chat..... And then he drops me into the most embarrassment ever....

 For you see... he... and seemingly all the other men in the ward... have all had dick operations.

 Yes, that's right... In a room full of men about to have their john-thomas tucked or worse I made a joke about "Guess what I'm having done".

I wanted the floor to open up and swallow me.

Everyone then promptly burst into laughter at the look on my face.

A couple were having later life circumcisions due to tight foreskins, one was having a lump removed, one was just have it biopsied or something... but all dick ops...

That's why they were silent and jealous of me with my big black felt marker arrow marking me out as the luckiest bastard in that room with the least to worry about.

I think about this today, my big black felt arrow, never have I been more relieved after hearing their procedure stories.  Though they went a little pale when I said "Oh I was awake during the op and watched".

Monday 4 March 2024

Tech Tribulations #2: There's a New System in Town

Time for another story from my tech history, this time I point my torch of memory at one of the first larger systems I worked on from start to finish (nearly 10 years).

I think I can say the product name of this particular system, it was called Phoenix and it wasn't a bad idea, to be honest, as with nearly every project I've ever worked on we needed more documentation and design; those to continuously neglected facets of software engineering.  But other than that, the initial design stood up.

Before we dive into that though, we need to understand the reason Phoenix was thrown onto a whiteboard and born.  What was the direct predecessor?

Yes, there was one, this is not a bluesky thinking system, Phoenix was to directly replace a system which had itself been engineered over the prior two years and not really lived up to expectations.

I shan't name that system, but anyone who knows me and knows Phoenix may know the code name it was given.  It was a hybrid of two languages a C wrapper around the physical hardware bound with JNI to a java core, which itself used the ODBC driver to talk to a Marathon database backend.

The idea being that the transactional nature of the database allowed for the representation of perfect state, and in any error it was simply a case of rolling the transaction off and so we had RORI behaviour in a very complex system.

The problem?

It was SSSSLLLLOOOOWWWWWW.

The host machine was (and I think I'm right here) a Pentium II class machine (it may have been a Pentium III later) with 16MB of RAM (and I believe it went up to 64MB by the end).  Am I talking about early 2005 here, we were working with legacy machines in the field which were being refitted from a system itself all written in C but not well understood.

This new modular java, database, transactional system was envisioned to be the future!

The problem?  Still it was SLLOOOOOOWWWWWW......

I've said that twice now, so for effect lets skip forward to about 2006, two years in and this database java monstrosity is not doing well, it doesn't perform well, the team has literally stopped even trying to work in agile scrum manner; the lead developer didn't take well to critique amongst other issues (like it's slow, have I said that three times now?)

Happily however there were two separate development machines, the one on the old old system and a whole other on the java junk.  So it was in the C group I was called into a meeting with my manager, the manager of the whole department.

He had a punch bag doll in front of a whiteboard and asked me to close the door.  Furtively expecting my P45 (to be fired) he asked me to take a look at the board... he had asked two other engineers to take a look through the day, so he was canvassing for opinion.

In the dry wipe ink was a block diagram, eight boxes, a hardware module to talk to hardware, a module to account, a module to host the menu, a module to talk to the content program, and so on, one for logging, one for sending reports back to HQ... These simple systems.

Each would talk to one another with a message passing mechanism, which was a line from each of these eight boxes.  Little did I know then that this message passing system would be the biggest part of my debugging life for the next ten years; that however is another story.

Right there and then this new system seemed like a much better proposition.

The manager liked the feedback from the various folks he had to talk to, and it was decided upon.  He would work on the platform, starting up the system, securing it with a manifest and preventing unauthorized execution of random code and content.

I would be working on the main attract module, this is the main menu from which content is categorized, and launched.

The content would talk to the Game Interface module, which was owned by a guy called Darren.

The accounting module was handled by the previous lead on the java system, and later a guy called Conrad.  Conrad at first though would be doing some other systems.

And there was a smattering of other work, like a logger module, and a build stack and al-sorts.

Work began, but not before the lead on the then current system had to be told her baby was being canned.  She and her main ally were duly summoned, shown the block diagram and where they would be working.  She did not take it well.

As she left the managers office, he was heard to exclaim and his stapler threw across the space and nailed a window barrier, sticking into it with a vibrating twang.

She loved her system, she did not like Phoenix, not least as it's name was a direct reference to something rising from the ashes, her system being declared to have self-combusted in failure.

Monday 26 February 2024

Tech Tribulations #1 : Smartcard Release Drama

It has been a very long time since a story time, so I thought I'd go over one about a software system I wrote from the ground up to secure the service to a machine; so I worked for a company which sold a whole machine to the customer (or leased them) while ever the buyer had the machines they would run.

In late 2014 the higher management realized this was an untapped revenue stream, and much to the annoyance of the customers, it was decided that a system update would go out; which the customer had to take to get any new content; and in this update they would have to also have a smart card reader installed and a card inserted which would count down time until it ran out.

Metering essentially, but "metering" had a whole other meaning for this system already, so it was just called the "Smartcard" system.

Really it was a subsystem, bolted into the main runtime as a thread check, which would wake at intervals, query if there was a card reader on the USB at all, check it was the exact brand of card reader (because we wanted to limit the customer just being able to put any in, they had to buy our pack).

And then it would query the card and deduce credit/count down if we were beyond a day.

We tried a bunch of time spans, hours, minutes etc, but deducting was decided to be after we accumulated 24 hours of on time, every 5 minutes an encrypted file on the disk would be marked, after 24 hours worth of accumulations the deduct would happen.

We tested this for months, absolutely months and to be honest we thought it was really robust.

Until it actually went into the customers hands, we suddenly had a slew of calls and returns, folks unhappy that they were inserting the card, "testing their machines" and suddenly all the credit was gone, and they were asking for a new card all the time.

At first we could simply not explain this anomaly, we had the written information about the service calls, replicated what the folks were saying, it all checked out fine, we got our increments, we could inspect the encrypted file and see we were accumulating normally and deducting normally.

I worked on this for days on end, we had to test for real, we did all sorts of things, power drop tests, pulling the card tests, all sorts.

The machine checked out, end of.

What had we missed?  What are the customers doing?  Testing the machine, okay, what are they testing?  The content, how does the content work for this update?  Well, seems that the customers didn't trust their testers or engineers, so what was happening was instead of testing for real they were doing what we called "Open door testing".

You see, when you close the door you accumulate and deduct, the machine is in operation normally as any user their end would have it operate....

Door open mode however, was intended to be used by service engineers, when the machine was deployed; so it is still in operation, the machine is in the field, but the door is briefly open to check things.

But these customers didn't trust their engineers in their warehouse, so they were not giving them credit to check the machine properly, they therefore tested in open mode... for days....

They accumulated massive operation debt with the machines in door open mode for days.

The moment they turned them off, happy they were working, and shipped them to sites they'd arrive on side immediately be turned on finally after so long in proper door closed operation and they'd instantly deduct the massive debt the warehouse team has accrued.

This was intentional.... But their use of the door open mode was an abuse, and one we had not even thought about.  We didn't even clock how long a machine sat in door open or door closed mode, worse still when in door open mode and test things on the machine ran at an accelerated update rate, we ticked over 10x faster to allow faster testing... The result was in just 3 days of warehouse door open mode testing they could accrue 30 days of operational debt.

That was a fault, one I could tackle with the team.  But changing the user habit of leaving the door open was harder...

We had to work with the user, and their patterns, we suspended the system for a short while and issued a new update, but the first customer taste of this "pay as you go" approach was a sour one.

Then things got bad....

Yes, you might think they were already bad, but they got worse.

A month later, all the above was resolved, and we thought things were settling down... Until we suddenly all hell broke loose.

EVERY MACHINE WAS LOCKED.

There were dozens of reports of their just not working, they had done their daily reboot and all of them reported a security fail on the smartcard....

All hands on deck, are our machines in the test pool doing the same?  Nope.

Is there something special going on?  Have clocks changed, is it a leap year, has the sky fallen on Chicken Little?

We honestly had no idea, there was no repeat in any of our test pool, no repeat on our personal engineering rigs, there was essentially no reason for this failure.

The only answer in such a situation is to observe or return one of the machines exhibiting the problem.

A lorry was sent and a machine brought back, under the explicit instruction not to open it nor change it, and the customer was not to keep their smartcard (it was theirs, but we would credit them a whole new card for the inconvenience).

Several hours spent staring at the code, and running checks by lowering cards so they would expire, or pulling the reader out and inserting it again we had no answer.

Before that arrives back with us however lets just think about the "smartcards" we used in our daily lives; our bank cards, they go into a machine we enter our pin and we remove them again.  Then how about cards like your gas meter, or you go see your GP, they have a card you insert into a machine and it stays there all the time to validate they are the GP, or they keep your meter in operation, if you have Sky TV and a viewing card; same thing, it is always in the device.

These machines are the latter kind.... Those cards are rated to have power on to them for long periods of time, as a consequence they cost more money than a card you only insert transiently...

And this company I worked for had very canny buyers, too canny.  Because they spotted a smartcard which used the same protocol... but was significantly less money to buy!

The difference?  You guessed it, it was the transient use variant.

The broken machine arrived, we powered it on, fail.  We open the door, remove the smartcard and sure enough on the rear of the plastic behind the chip the plastic is brown, burned.

The card can not be electrically trusted!

We highlight this and send it back to the buying department, they fouled up, they changed the hardware after we certified it, essentially sending an uncertified machine out.

A huge issue ensued about this, as this wasn't well understood that we had been provided and advised one card type into the update set, but of course the buyers would not accept it wasn't the same until we literally had the specifications of the card side by side we could see a digit difference in the part number and looked up the datasheet where clearly it said that the transient card was only rated to remain in a machine for 10 minutes.  More than enough for an ATM.  But a "security gating" card, as we wanted, they are rated to be inserted continually for 36 months.

Monday 8 January 2024

The 2023 Great Laptop Shop...

It is that time of life again.... Time to shop for a new Laptop... And I am finding a new scourge in the mix, that is "Soldered".

I am so absolutely sick of seeing "Soldered" next to the RAM specifications, like I get it for budget machines, I get it for tiny integrations, but for actual working laptops?  For laptops marketed as "Business" or as "performance"... oh my word, why are they doing this?

I really hate this whole phase.  I have hated the laptop hidden gotcha's for years to be honest, and I'm upset Lenovo have joined the price crunch unupgradable ewaste train.

My current machine being a Lenovo E480, I can and have upgraded the RAM, it has two drive slots (one SATA and one nVME), you can change the battery and it has plenty of USB and aux IO ports all around.  The issues I have with it is that one RAM Slot has gone bad; I simply doubled the RAM in the one remaining slot - but really I'm not at end of life.  And the CPU being an 8th gen is struggling with my workloads.

My options are to soldier on with the E480, maybe even move all my work into remote virtual machines and use this simply as a terminal into them; but then I would always require network access and that's not always possible.

Or get something new and locked down.

Or a Framework offering, which lets me upgrade to a point.

Or keep shopping... But the ease of finding what I want is very much on the low-side, and I'm a technical user, finding what I want should be simple.  But the deck seems stacked with both retailers and system integrators wanting to pass off this shovel ware crap on me.

Monday 23 October 2023

Job Interview : switch(char) again! Twenty years on!

I've spoken about some of the strange interviews I've enjoyed over the years; notable engineering highlights were having test driven development spooned to me, when I simply didn't know the particular syntax they were demanding, or having StringBuilder suggested for a C# task where they actually needed to cross-core thread their problem, or pointing out that the CI solution being described would lead to an alien environment of folks being paralysed by fear of red flags; only to witness their development floor being paralysed by exactly those issues as I walked out the door.

Today however, I'm going to go way back and talk about one of the very early development interviews I had; this maybe like the third interview I ever had after graduating, it was with a rail rolling stock company, in a very bright and sunny room with three guys who seemed to know their entire universe was correct and they'd take no new information into their sphere of understanding.

I share this one as a friend of mine was in touch over the weekend, he had an interview, and strangely he had the exact same conversation I had... THE EXACT CONVERSATION, twenty something years later.

Let us start by sharing some perfectly valid C++ code:

bool validate(const char& value)
{
switch (value)
{
case 0: return true;
case 'x': return true;
default: return false;
}
return false;
}
int main()
{
const bool isValidOne{ validate('a') };
const bool isValidTwo{ validate('x') };
const bool isValidMe{ validate(12) };
}

Read this code, we should see an output (if we output the bools) of "false", "true" and "false"... I think we can all agree.  This is perfectly valid C++.

It isn't very clean nor pretty, we're relying on the compiler standard decaying char to int and vice versa; which is not very nice and I wish compilers would complain here, that we're comparing int to char in the first case and we're converting the integer 12 to char, I think it should really strong type this.

But that is not the point, the point is that both in my friends case and mine all those years ago the interviewers got really uppity about that "switch" statement.  They insisted that you can not "switch" on a character type...

Just flat insisted.

I remember in my experience they had me write up what they wanted on a blackboard, with chalk, and they wanted me to go through the code line by line; I wrote more or less the above, they insisted they had a character not a byte (uint8_t: though there was no standard for this at the time) and then they went off on this tangent amongst themselves about the value of the if-elseif-else stanza's they insisted would work over my solution above.

Now I was talking to these guys in the early 00's, my friend however was talking to them in 2023.  He jumped on this with the point of "well if the character is known at compile time, yes then constexpr maybe of use" and he showed them:

if constexpr(foo) and if consteval(bar) examples.

He described their faces screwing up and two of the chaps he was talking to staring at each other before making subtle notes on their paper.

It was very clear at this point that the CTO performing the interview was not keeping his knowledge up to date, the group seemed to cluster onto the idea of just maintaining what they have, stagnation like this in our joint experience can be problematic.  Both team morale and high turn over of engineers always happens when things stagnate.

I have to say in my interview I actually sat it out from beginning to end and just knew I was not interested in the role; and I did not hear from them again.

My friend however, he was genuinely concerned he'd done a good interview and they'd be offering him a role, they kept him talking for over an hour and a half!

He's a late 40's engineer of 25+ years experience, just like me; the three chaps interviewing him, including the CTO, were ten or more years his junior, it seemed they were looking to back up their team with an older head to lend legitimacy to things; and they had lost two original engineers of their project to literal retirement without replacing them.

He saw the issues and he thanked them for his time and left, with a negative to whether he would be interested in going further.  This stunned them.

It has to be said, so many interviewers forget that though they are interviewing you, you are also interviewing them like most all human interaction it is a two way street and it's crucially important to remember that in a job interview.  We spent a significant amount of our time at work, if you are not comfortable there or feel you are going to be acting out a role instead of filling one and making it your own.